package org.cloudfoundry.identity.uaa.login; import org.cloudfoundry.identity.uaa.error.UaaException; import org.cloudfoundry.identity.uaa.login.test.ThymeleafConfig; import org.codehaus.jackson.map.ObjectMapper; import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; import org.springframework.mock.http.client.MockClientHttpResponse; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.test.web.client.ResponseCreator; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.thymeleaf.spring4.SpringTemplateEngine; import java.io.IOException; import java.sql.Timestamp; import java.util.HashMap; import java.util.Map; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.junit.Assert.*; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; import static org.springframework.http.HttpMethod.GET; import static org.springframework.http.HttpMethod.POST; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.CONFLICT; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath; import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ThymeleafConfig.class) public class EmailAccountCreationServiceTests { private EmailAccountCreationService emailAccountCreationService; private MockRestServiceServer mockUaaServer; private MessageService messageService; private RestTemplate uaaTemplate; @Autowired @Qualifier("mailTemplateEngine") SpringTemplateEngine templateEngine; @Before public void setUp() throws Exception { uaaTemplate = new RestTemplate(); mockUaaServer = MockRestServiceServer.createServer(uaaTemplate); messageService = mock(MessageService.class); emailAccountCreationService = new EmailAccountCreationService(new ObjectMapper(), templateEngine, messageService, uaaTemplate, "http://uaa.example.com", "pivotal", "http://login.example.com"); } @Test public void testBeginActivation() throws Exception { setUpForSuccess(); emailAccountCreationService.beginActivation("user@example.com", "password", "login"); mockUaaServer.verify(); ArgumentCaptor<String> emailBodyArgument = ArgumentCaptor.forClass(String.class); verify(messageService).sendMessage( eq("newly-created-user-id"), eq("user@example.com"), eq(MessageType.CREATE_ACCOUNT_CONFIRMATION), eq("Activate your Pivotal ID"), emailBodyArgument.capture() ); String emailBody = emailBodyArgument.getValue(); assertThat(emailBody, containsString("a Pivotal ID")); assertThat(emailBody, containsString("<a href=\"http://login.example.com/verify_user?code=the_secret_code&email=user%40example.com\">Activate your account</a>")); assertThat(emailBody, not(containsString("Cloud Foundry"))); } @Test public void testBeginActivationWithOssBrand() throws Exception { emailAccountCreationService = new EmailAccountCreationService(new ObjectMapper(), templateEngine, messageService, uaaTemplate, "http://uaa.example.com", "oss", "http://login.example.com"); setUpForSuccess(); emailAccountCreationService.beginActivation("user@example.com", "password", "login"); ArgumentCaptor<String> emailBodyArgument = ArgumentCaptor.forClass(String.class); verify(messageService).sendMessage( eq("newly-created-user-id"), eq("user@example.com"), eq(MessageType.CREATE_ACCOUNT_CONFIRMATION), eq("Activate your account"), emailBodyArgument.capture() ); String emailBody = emailBodyArgument.getValue(); assertThat(emailBody, containsString("an account")); assertThat(emailBody, containsString("<a href=\"http://login.example.com/verify_user?code=the_secret_code&email=user%40example.com\">Activate your account</a>")); assertThat(emailBody, not(containsString("Pivotal"))); } @Test(expected = UaaException.class) public void testBeginActivationWithExistingUser() throws Exception { mockUaaServer.expect(requestTo("http://uaa.example.com/Users")) .andExpect(method(POST)) .andExpect(jsonPath("$.userName").value("user@example.com")) .andExpect(jsonPath("$.password").value("password")) .andExpect(jsonPath("$.origin").value("uaa")) .andExpect(jsonPath("$.emails[0].value").value("user@example.com")) .andRespond(new ResponseCreator() { @Override public ClientHttpResponse createResponse(ClientHttpRequest request) throws IOException { return new MockClientHttpResponse("{\"error\":\"invalid_user\",\"message\":\"error message\",\"user_id\":\"existing-user-id\",\"verified\":true,\"active\":true}".getBytes(), CONFLICT); } }); emailAccountCreationService.beginActivation("user@example.com", "password", "login"); } @Test public void testBeginActivationWithUnverifiedExistingUser() throws Exception { mockUaaServer.expect(requestTo("http://uaa.example.com/Users")) .andExpect(method(POST)) .andExpect(jsonPath("$.userName").value("user@example.com")) .andExpect(jsonPath("$.password").value("password")) .andExpect(jsonPath("$.origin").value("uaa")) .andExpect(jsonPath("$.emails[0].value").value("user@example.com")) .andRespond(new ResponseCreator() { @Override public ClientHttpResponse createResponse(ClientHttpRequest request) throws IOException { return new MockClientHttpResponse("{\"error\":\"invalid_user\",\"message\":\"error message\",\"user_id\":\"existing-user-id\",\"verified\":false,\"active\":true}".getBytes(), CONFLICT); } }); String uaaResponseJson = "{" + " \"code\":\"the_secret_code\"," + " \"expiresAt\":9999999999," + " \"data\":\"{\\\"user_id\\\":\\\"existing-user-id\\\",\\\"client_id\\\":\\\"login\\\"}\"" + "}"; mockUaaServer.expect(requestTo("http://uaa.example.com/Codes")) .andExpect(method(POST)) .andExpect(jsonPath("$.data").value("{\"user_id\":\"existing-user-id\",\"client_id\":\"login\"}")) .andRespond(withSuccess(uaaResponseJson, APPLICATION_JSON)); MockHttpServletRequest request = new MockHttpServletRequest(); request.setProtocol("http"); request.setContextPath("/login"); RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); emailAccountCreationService.beginActivation("user@example.com", "password", "login"); verify(messageService).sendMessage( eq("existing-user-id"), eq("user@example.com"), eq(MessageType.CREATE_ACCOUNT_CONFIRMATION), anyString(), anyString() ); } @Test public void testCompleteActivation() throws Exception { Timestamp ts = new Timestamp(System.currentTimeMillis() + (60 * 60 * 1000)); // 1 hour String uaaResponseJson = "{" + " \"code\":\"the_secret_code\"," + " \"expiresAt\":" + ts.getTime() + "," + " \"data\":\"{\\\"user_id\\\":\\\"newly-created-user-id\\\",\\\"client_id\\\":\\\"app\\\"}\"" + "}"; mockUaaServer.expect(requestTo("http://uaa.example.com/Codes/the_secret_code")) .andExpect(method(GET)) .andRespond(withSuccess(uaaResponseJson, APPLICATION_JSON)); String scimUserJSONString = "{" + "\"userName\": \"user@example.com\"," + "\"id\": \"newly-created-user-id\"," + "\"emails\": [{\"value\":\"user@example.com\"}]" + "}"; mockUaaServer.expect(requestTo("http://uaa.example.com/Users/newly-created-user-id/verify")) .andExpect(method(GET)) .andRespond(withSuccess(scimUserJSONString, APPLICATION_JSON)); Map<String,Object> additionalInformation = new HashMap<>(); additionalInformation.put("signup_redirect_url", "http://example.com/redirect"); String clientDetails = "{" + "\"client_id\": \"app\"," + "\"signup_redirect_url\": \"http://example.com/redirect\"" + "}"; mockUaaServer.expect(requestTo("http://uaa.example.com/oauth/clients/app")) .andExpect(method(GET)) .andRespond(withSuccess(clientDetails, APPLICATION_JSON)); AccountCreationService.AccountCreationResponse accountCreation = emailAccountCreationService.completeActivation("the_secret_code"); mockUaaServer.verify(); assertEquals("user@example.com", accountCreation.getUsername()); assertEquals("newly-created-user-id", accountCreation.getUserId()); assertEquals("http://example.com/redirect", accountCreation.getRedirectLocation()); assertNotNull(accountCreation.getUserId()); } @Test public void testCompleteActivationWithExpiredCode() throws Exception { mockUaaServer.expect(requestTo("http://uaa.example.com/Codes/expiring_code")) .andExpect(method(GET)) .andRespond(withStatus(BAD_REQUEST)); try { emailAccountCreationService.completeActivation("expiring_code"); fail(); } catch(HttpClientErrorException e) { assertThat(e.getStatusCode(), Matchers.equalTo(BAD_REQUEST)); } } @Test public void testResendVerificationCode() throws Exception { String uaaResponse = "{\n" + " \"resources\": [\n" + " {\n" + " \"id\": \"unverified-user-id\",\n" + " \"userName\": \"user@example.com\",\n" + " \"origin\": \"uaa\"\n" + " }\n" + " ],\n" + " \"startIndex\": 1,\n" + " \"itemsPerPage\": 100,\n" + " \"totalResults\": 1,\n" + " \"schemas\": [\n" + " \"urn:scim:schemas:core:1.0\"\n" + " ]\n" + "}"; mockUaaServer.expect(requestTo("http://uaa.example.com/ids/Users?attributes=id&filter=userName%20eq%20%22user@example.com%22%20and%20origin%20eq%20%22uaa%22")) .andExpect(method(GET)) .andRespond(withSuccess(uaaResponse, APPLICATION_JSON)); Timestamp ts = new Timestamp(System.currentTimeMillis() + (60 * 60 * 1000)); // 1 hour String uaaResponseJson = "{" + " \"code\":\"the_secret_code\"," + " \"expiresAt\":" + ts.getTime() + "," + " \"data\":\"{\\\"user_id\\\":\\\"unverified-user-id\\\",\\\"client_id\\\":\\\"login\\\"}\"" + "}"; mockUaaServer.expect(requestTo("http://uaa.example.com/Codes")) .andExpect(method(POST)) .andExpect(jsonPath("$.expiresAt").value(Matchers.greaterThan(ts.getTime() - 5000))) .andExpect(jsonPath("$.expiresAt").value(Matchers.lessThan(ts.getTime() + 5000))) .andExpect(jsonPath("$.data").exists()) // we can't tell what order the json keys will take in the serialized json, so exists is the best we can do .andRespond(withSuccess(uaaResponseJson, APPLICATION_JSON)); MockHttpServletRequest request = new MockHttpServletRequest(); request.setProtocol("http"); request.setContextPath("/login"); RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); emailAccountCreationService.resendVerificationCode("user@example.com", "login"); mockUaaServer.verify(); ArgumentCaptor<String> emailBodyArgument = ArgumentCaptor.forClass(String.class); verify(messageService).sendMessage(eq("unverified-user-id"), eq("user@example.com"), eq(MessageType.CREATE_ACCOUNT_CONFIRMATION), eq("Activate your Pivotal ID"), emailBodyArgument.capture() ); String emailBody = emailBodyArgument.getValue(); assertThat(emailBody, containsString("a Pivotal ID")); assertThat(emailBody, containsString("<a href=\"http://login.example.com/verify_user?code=the_secret_code&email=user%40example.com\">Activate your account</a>")); assertThat(emailBody, not(containsString("Cloud Foundry"))); } private void setUpForSuccess() { String scimUserJSONString = "{" + "\"userName\": \"user@example.com\"," + "\"id\": \"newly-created-user-id\"," + "\"emails\": [{\"value\":\"user@example.com\"}]" + "}"; mockUaaServer.expect(requestTo("http://uaa.example.com/Users")) .andExpect(method(POST)) .andExpect(jsonPath("$.userName").value("user@example.com")) .andExpect(jsonPath("$.password").value("password")) .andExpect(jsonPath("$.origin").value("uaa")) .andExpect(jsonPath("$.active").value(true)) .andExpect(jsonPath("$.verified").value(false)) .andExpect(jsonPath("$.emails[0].value").value("user@example.com")) .andRespond(withSuccess(scimUserJSONString, APPLICATION_JSON)); Timestamp ts = new Timestamp(System.currentTimeMillis() + (60 * 60 * 1000)); // 1 hour String uaaResponseJson = "{" + " \"code\":\"the_secret_code\"," + " \"expiresAt\":" + ts.getTime() + "," + " \"data\":\"{\\\"user_id\\\":\\\"newly-created-user-id\\\",\\\"client_id\\\":\\\"login\\\"}\"" + "}"; mockUaaServer.expect(requestTo("http://uaa.example.com/Codes")) .andExpect(method(POST)) .andExpect(jsonPath("$.expiresAt").value(Matchers.greaterThan(ts.getTime() - 5000))) .andExpect(jsonPath("$.expiresAt").value(Matchers.lessThan(ts.getTime() + 5000))) .andExpect(jsonPath("$.data").exists()) // we can't tell what order the json keys will take in the serialized json, so exists is the best we can do .andRespond(withSuccess(uaaResponseJson, APPLICATION_JSON)); MockHttpServletRequest request = new MockHttpServletRequest(); request.setProtocol("http"); request.setContextPath("/login"); RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); } }