/*******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*******************************************************************************/
package org.cloudfoundry.identity.uaa.login;
import org.cloudfoundry.identity.uaa.TestClassNullifier;
import org.cloudfoundry.identity.uaa.account.ConflictException;
import org.cloudfoundry.identity.uaa.account.ForgotPasswordInfo;
import org.cloudfoundry.identity.uaa.account.NotFoundException;
import org.cloudfoundry.identity.uaa.account.ResetPasswordController;
import org.cloudfoundry.identity.uaa.account.ResetPasswordService;
import org.cloudfoundry.identity.uaa.codestore.ExpiringCode;
import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore;
import org.cloudfoundry.identity.uaa.codestore.InMemoryExpiringCodeStore;
import org.cloudfoundry.identity.uaa.message.MessageService;
import org.cloudfoundry.identity.uaa.message.MessageType;
import org.cloudfoundry.identity.uaa.user.UaaUser;
import org.cloudfoundry.identity.uaa.user.UaaUserDatabase;
import org.cloudfoundry.identity.uaa.zone.BrandingInformation;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.thymeleaf.spring4.SpringTemplateEngine;
import java.sql.Timestamp;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.contains;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ThymeleafAdditional.class,ThymeleafConfig.class})
public class ResetPasswordControllerTest extends TestClassNullifier {
private MockMvc mockMvc;
private ResetPasswordService resetPasswordService;
private MessageService messageService;
private ExpiringCodeStore codeStore;
private UaaUserDatabase userDatabase;
private String companyName = "Best Company";
@Autowired
@Qualifier("mailTemplateEngine")
private SpringTemplateEngine templateEngine;
private AccountSavingAuthenticationSuccessHandler successHandler = new AccountSavingAuthenticationSuccessHandler();
@Before
public void setUp() throws Exception {
SecurityContextHolder.clearContext();
IdentityZoneHolder.set(IdentityZone.getUaa());
resetPasswordService = mock(ResetPasswordService.class);
messageService = mock(MessageService.class);
codeStore = new InMemoryExpiringCodeStore();
userDatabase = mock(UaaUserDatabase.class);
when(userDatabase.retrieveUserById(anyString())).thenReturn(new UaaUser("username","password","email","givenname","familyname"));
ResetPasswordController controller = new ResetPasswordController(resetPasswordService, messageService, templateEngine, codeStore, userDatabase, successHandler);
mockMvc = MockMvcBuilders
.standaloneSetup(controller)
.setViewResolvers(getResolver())
.build();
}
@After
public void tearDown() {
SecurityContextHolder.clearContext();
IdentityZoneHolder.set(IdentityZone.getUaa());
}
@Test
public void testForgotPasswordPage() throws Exception {
mockMvc.perform(get("/forgot_password")
.param("client_id", "example")
.param("redirect_uri", "http://example.com"))
.andExpect(status().isOk())
.andExpect(view().name("forgot_password"))
.andExpect(model().attribute("client_id", "example"))
.andExpect(model().attribute("redirect_uri", "http://example.com"));
}
@Test
public void testForgotPasswordWithSelfServiceDisabled() throws Exception {
IdentityZone zone = MultitenancyFixture.identityZone("test-zone-id", "testsubdomain");
zone.getConfig().getLinks().getSelfService().setSelfServiceLinksEnabled(false);
IdentityZoneHolder.set(zone);
mockMvc.perform(get("/forgot_password")
.param("client_id", "example")
.param("redirect_uri", "http://example.com"))
.andExpect(status().isNotFound())
.andExpect(view().name("error"))
.andExpect(model().attribute("error_message_code", "self_service_disabled"));
}
@Test
public void forgotPassword_Conflict_SendsEmailWithUnavailableEmailHtml() throws Exception {
forgotPasswordWithConflict(null, companyName);
}
@Test
public void forgotPassword_ConflictInOtherZone_SendsEmailWithUnavailableEmailHtml() throws Exception {
String subdomain = "testsubdomain";
IdentityZoneHolder.set(MultitenancyFixture.identityZone("test-zone-id", subdomain));
forgotPasswordWithConflict(subdomain, "The Twiglet Zone");
}
private void forgotPasswordWithConflict(String zoneDomain, String companyName) throws Exception {
IdentityZoneConfiguration defaultConfig = IdentityZoneHolder.get().getConfig();
BrandingInformation branding = new BrandingInformation();
branding.setCompanyName(companyName);
IdentityZoneConfiguration config = new IdentityZoneConfiguration();
config.setBranding(branding);
IdentityZoneHolder.get().setConfig(config);
try {
new ResetPasswordController(resetPasswordService, messageService, templateEngine, codeStore, userDatabase, successHandler);
String domain = zoneDomain == null ? "localhost" : zoneDomain + ".localhost";
when(resetPasswordService.forgotPassword("user@example.com", "", "")).thenThrow(new ConflictException("abcd"));
MockHttpServletRequestBuilder post = post("/forgot_password.do")
.contentType(APPLICATION_FORM_URLENCODED)
.param("email", "user@example.com");
post.with(request -> {
request.setServerName(domain);
return request;
});
mockMvc.perform(post)
.andExpect(status().isFound())
.andExpect(redirectedUrl("email_sent?code=reset_password"));
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
Mockito.verify(messageService).sendMessage(
eq("user@example.com"),
eq(MessageType.PASSWORD_RESET),
eq(companyName + " account password reset request"),
captor.capture()
);
String emailContent = captor.getValue();
assertThat(emailContent, containsString(String.format("A request has been made to reset your %s account password for %s", companyName, "user@example.com")));
assertThat(emailContent, containsString("Your account credentials for " + domain + " are managed by an external service. Please contact your administrator for password recovery requests."));
assertThat(emailContent, containsString("Thank you,<br />\n " + companyName));
} finally {
IdentityZoneHolder.get().setConfig(defaultConfig);
}
}
@Test
public void forgotPassword_DoesNotSendEmail_UserNotFound() throws Exception {
when(resetPasswordService.forgotPassword("user@example.com", "", "")).thenThrow(new NotFoundException());
MockHttpServletRequestBuilder post = post("/forgot_password.do")
.contentType(APPLICATION_FORM_URLENCODED)
.param("email", "user@example.com");
mockMvc.perform(post)
.andExpect(status().isFound())
.andExpect(redirectedUrl("email_sent?code=reset_password"));
Mockito.verifyZeroInteractions(messageService);
}
@Test
public void forgotPassword_Successful() throws Exception {
forgotPasswordSuccessful("http://localhost/reset_password?code=code1");
}
@Test
public void forgotPassword_SuccessfulDefaultCompanyName() throws Exception {
ResetPasswordController controller = new ResetPasswordController(resetPasswordService, messageService, templateEngine, codeStore, userDatabase, successHandler);
mockMvc = MockMvcBuilders
.standaloneSetup(controller)
.setViewResolvers(getResolver())
.build();
forgotPasswordSuccessful("http://localhost/reset_password?code=code1", "Cloud Foundry");
}
@Test
public void forgotPassword_SuccessfulInOtherZone() throws Exception {
IdentityZone zone = MultitenancyFixture.identityZone("test-zone-id", "testsubdomain");
IdentityZoneHolder.set(zone);
forgotPasswordSuccessful("http://testsubdomain.localhost/reset_password?code=code1", "The Twiglet Zone");
}
@Test
public void forgotPasswordPostWithSelfServiceDisabled() throws Exception {
IdentityZone zone = MultitenancyFixture.identityZone("test-zone-id", "testsubdomain");
zone.getConfig().getLinks().getSelfService().setSelfServiceLinksEnabled(false);
IdentityZoneHolder.set(zone);
mockMvc.perform(post("/forgot_password.do")
.contentType(APPLICATION_FORM_URLENCODED)
.param("email", "user@example.com")
.param("client_id", "example")
.param("redirect_uri", "redirect.example.com"))
.andExpect(status().isNotFound())
.andExpect(view().name("error"))
.andExpect(model().attribute("error_message_code", "self_service_disabled"));
}
private void forgotPasswordSuccessful(String url) throws Exception {
forgotPasswordSuccessful(url, "Best Company");
}
private void forgotPasswordSuccessful(String url, String companyName) throws Exception {
IdentityZoneConfiguration defaultConfig = IdentityZoneHolder.get().getConfig();
BrandingInformation branding = new BrandingInformation();
branding.setCompanyName(companyName);
IdentityZoneConfiguration config = new IdentityZoneConfiguration();
config.setBranding(branding);
IdentityZoneHolder.get().setConfig(config);
try {
when(resetPasswordService.forgotPassword("user@example.com", "example", "redirect.example.com")).thenReturn(new ForgotPasswordInfo("123", new ExpiringCode("code1", new Timestamp(System.currentTimeMillis()), "someData", null)));
MockHttpServletRequestBuilder post = post("/forgot_password.do")
.contentType(APPLICATION_FORM_URLENCODED)
.param("email", "user@example.com")
.param("client_id", "example")
.param("redirect_uri", "redirect.example.com");
if (!IdentityZoneHolder.isUaa()) {
post.with(request -> {
request.setServerName(IdentityZoneHolder.get().getSubdomain() + ".localhost");
return request;
});
}
mockMvc.perform(post)
.andExpect(status().isFound())
.andExpect(redirectedUrl("email_sent?code=reset_password"));
verify(messageService).sendMessage(
eq("user@example.com"),
eq(MessageType.PASSWORD_RESET),
eq(companyName + " account password reset request"),
contains("<a href=\"" + url + "\">Reset your password</a>")
);
} finally {
IdentityZoneHolder.get().setConfig(defaultConfig);
}
}
@Test
public void testForgotPasswordFormValidationFailure() throws Exception {
MockHttpServletRequestBuilder post = post("/forgot_password.do")
.contentType(APPLICATION_FORM_URLENCODED)
.param("email", "notAnEmail");
mockMvc.perform(post)
.andExpect(status().isUnprocessableEntity())
.andExpect(view().name("forgot_password"))
.andExpect(model().attribute("message_code", "form_error"));
verifyZeroInteractions(resetPasswordService);
}
@Test
public void testInstructions() throws Exception {
mockMvc.perform(get("/email_sent").param("code", "reset_password"))
.andExpect(status().isOk())
.andExpect(model().attribute("code", "reset_password"));
}
@Test
public void testResetPasswordPage() throws Exception {
ExpiringCode code = codeStore.generateCode("{\"user_id\" : \"some-user-id\"}", new Timestamp(System.currentTimeMillis() + 1000000), null);
mockMvc.perform(get("/reset_password").param("email", "user@example.com").param("code", code.getCode()))
.andExpect(status().isOk())
.andExpect(view().name("reset_password"));
}
@Test
public void testResetPasswordPageDuplicate() throws Exception {
ExpiringCode code = codeStore.generateCode("{\"user_id\" : \"some-user-id\"}", new Timestamp(System.currentTimeMillis() + 1000000), null);
mockMvc.perform(get("/reset_password").param("email", "user@example.com").param("code", code.getCode()))
.andExpect(status().isOk())
.andExpect(view().name("reset_password"));
mockMvc.perform(get("/reset_password").param("email", "user@example.com").param("code", code.getCode()))
.andExpect(status().isUnprocessableEntity())
.andExpect(view().name("forgot_password"));
}
@Test
public void testResetPasswordPageWhenExpiringCodeNull() throws Exception {
mockMvc.perform(get("/reset_password").param("email", "user@example.com").param("code", "code1"))
.andExpect(status().isUnprocessableEntity())
.andExpect(view().name("forgot_password"))
.andExpect(model().attribute("message_code", "bad_code"));
}
}