Skip to content
Permalink
Browse files Browse the repository at this point in the history
XWIKI-18384: Improve ForgotUsername process
  * Ensure to send an email to users in case of forgot username request
  * Improve test

(cherry picked from commit 21f8780)
  • Loading branch information
surli committed Mar 5, 2021
1 parent 783d07a commit f0440df
Show file tree
Hide file tree
Showing 5 changed files with 400 additions and 32 deletions.
Expand Up @@ -19,12 +19,30 @@
*/
package org.xwiki.administration.test.ui;

import java.util.HashMap;
import java.util.Map;

import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.internet.MimeMessage;

import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.xwiki.administration.test.po.ForgotUsernameCompletePage;
import org.xwiki.administration.test.po.ForgotUsernamePage;
import org.xwiki.test.docker.junit5.TestConfiguration;
import org.xwiki.test.docker.junit5.UITest;
import org.xwiki.test.integration.junit.LogCaptureConfiguration;
import org.xwiki.test.ui.TestUtils;

import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetupTest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand All @@ -34,44 +52,171 @@
* @version $Id$
* @since 11.10
*/
@UITest
@UITest(sshPorts = {
// Open the GreenMail port so that the XWiki instance inside a Docker container can use the SMTP server provided
// by GreenMail running on the host.
3025
},
properties = {
// The Mail module contributes a Hibernate mapping that needs to be added to hibernate.cfg.xml
"xwikiDbHbmCommonExtraMappings=mailsender.hbm.xml"
},
extraJARs = {
// It's currently not possible to install a JAR contributing a Hibernate mapping file as an Extension. Thus
// we need to provide the JAR inside WEB-INF/lib. See https://jira.xwiki.org/browse/XWIKI-8271
"org.xwiki.platform:xwiki-platform-mail-send-storage"
}
)
public class ForgotUsernameIT
{
private GreenMail mail;

@BeforeEach
public void startMail(TestUtils setup, TestConfiguration testConfiguration)
{
this.mail = new GreenMail(ServerSetupTest.SMTP);
this.mail.start();

configureEmail(setup, testConfiguration);
}

@AfterEach
public void stopMail(TestUtils setup, LogCaptureConfiguration logCaptureConfiguration)
{
if (this.mail != null) {
this.mail.stop();
}

restoreSettings(setup);
logCaptureConfiguration.registerExcludes("CSRFToken: Secret token verification failed, token");
}

private void configureEmail(TestUtils setup, TestConfiguration testConfiguration)
{
setup.updateObject("Mail", "MailConfig", "Mail.SendMailConfigClass", 0, "host",
testConfiguration.getServletEngine().getHostIP(), "port", "3025", "sendWaitTime", "0");
}

private void restoreSettings(TestUtils setup)
{
// Make sure we can restore the settings, so we log back with superadmin to finish the work
setup.loginAsSuperAdmin();

// Remove the previous version that the setup has created.
setup.deleteLatestVersion("Mail", "MailConfig");
}

private Map<String, String> getMessageContent(MimeMessage message) throws Exception
{
Map<String, String> messageMap = new HashMap<>();

Address[] addresses = message.getAllRecipients();
assertTrue(addresses.length == 1);
messageMap.put("recipient", addresses[0].toString());

messageMap.put("subjectLine", message.getSubject());

Multipart mp = (Multipart) message.getContent();

BodyPart plain = getPart(mp, "text/plain");
if (plain != null) {
messageMap.put("textPart", IOUtils.toString(plain.getInputStream(), "UTF-8"));
}
BodyPart html = getPart(mp, "text/html");
if (html != null) {
messageMap.put("htmlPart", IOUtils.toString(html.getInputStream(), "UTF-8"));
}

return messageMap;
}

private BodyPart getPart(Multipart messageContent, String mimeType) throws Exception
{
for (int i = 0; i < messageContent.getCount(); i++) {
BodyPart part = messageContent.getBodyPart(i);

if (part.isMimeType(mimeType)) {
return part;
}

if (part.isMimeType("multipart/related") || part.isMimeType("multipart/alternative")
|| part.isMimeType("multipart/mixed"))
{
BodyPart out = getPart((Multipart) part.getContent(), mimeType);
if (out != null) {
return out;
}
}
}
return null;
}

@Test
public void retrieveUsername(TestUtils testUtils)
public void retrieveUsername(TestUtils testUtils) throws Exception
{
String user = "realuser";
String userMail = "realuser@host.org";
// We create three users, two of them are sharing the same email
String user1Login = "realuser1";
String user1Email = "realuser@host.org";

String user2Login = "realuser2";
String user2Email = "realuser@host.org";

String user3Login = "foo";
String user3Email = "foo@host.org";

// We need to login as superadmin to set the user email.
testUtils.loginAsSuperAdmin();
testUtils.createUser(user, "realuserpwd", testUtils.getURLToNonExistentPage(), "email", userMail);
testUtils.createUser(user1Login, "realuserpwd", testUtils.getURLToNonExistentPage(), "email", user1Email);
testUtils.createUser(user2Login, "realuserpwd", testUtils.getURLToNonExistentPage(), "email", user2Email);
testUtils.createUser(user3Login, "realuserpwd", testUtils.getURLToNonExistentPage(), "email", user3Email);

testUtils.forceGuestUser();

// check that when asking to retrieve username with a wrong email we don't get any information
// if an user exists or not and no email is sent.
ForgotUsernamePage forgotUsernamePage = ForgotUsernamePage.gotoPage();
forgotUsernamePage.setEmail(userMail);
forgotUsernamePage.setEmail("notexistant@xwiki.com");
ForgotUsernameCompletePage forgotUsernameCompletePage = forgotUsernamePage.clickRetrieveUsername();
assertFalse(forgotUsernameCompletePage.isAccountNotFound());
assertTrue(forgotUsernameCompletePage.isUsernameRetrieved(user));
assertTrue(forgotUsernameCompletePage.isForgotUsernameQuerySent());

// we are waiting 5 sec here just to be sure no mail is sent, maybe we could decrease the timeout value,
// not sure.
assertFalse(this.mail.waitForIncomingEmail(1));

// Bypass the check that prevents to reload the current page
testUtils.gotoPage(testUtils.getURLToNonExistentPage());

// test that bad mail results in no results
// test getting email for a forgot username request where the email is set in one account only
forgotUsernamePage = ForgotUsernamePage.gotoPage();
forgotUsernamePage.setEmail("bad_mail@evil.com");
forgotUsernamePage.setEmail(user3Email);
forgotUsernameCompletePage = forgotUsernamePage.clickRetrieveUsername();
assertTrue(forgotUsernameCompletePage.isAccountNotFound());
assertFalse(forgotUsernameCompletePage.isUsernameRetrieved(user));
assertTrue(forgotUsernameCompletePage.isForgotUsernameQuerySent());
assertTrue(this.mail.waitForIncomingEmail(1));
MimeMessage[] receivedEmails = this.mail.getReceivedMessages();
assertEquals(1, receivedEmails.length);
MimeMessage receivedEmail = receivedEmails[0];
assertTrue(receivedEmail.getSubject().contains("Forgot username on"));
String receivedMailContent = getMessageContent(receivedEmail).get("textPart");
assertTrue(receivedMailContent.contains(String.format("XWiki.%s", user3Login)));

// remove mails for last test
this.mail.purgeEmailFromAllMailboxes();

// Bypass the check that prevents to reload the current page
testUtils.gotoPage(testUtils.getURLToNonExistentPage());

// XWIKI-4920 test that the email is properly escaped
// test getting email for a forgot username request where the email is set in two accounts
forgotUsernamePage = ForgotUsernamePage.gotoPage();
forgotUsernamePage.setEmail("a' synta\\'x error");
forgotUsernamePage.setEmail(user1Email);
forgotUsernameCompletePage = forgotUsernamePage.clickRetrieveUsername();
assertTrue(forgotUsernameCompletePage.isAccountNotFound());
assertFalse(forgotUsernameCompletePage.isUsernameRetrieved(user));
assertTrue(forgotUsernameCompletePage.isForgotUsernameQuerySent());
assertTrue(this.mail.waitForIncomingEmail(1));
receivedEmails = this.mail.getReceivedMessages();
assertEquals(1, receivedEmails.length);
receivedEmail = receivedEmails[0];
assertTrue(receivedEmail.getSubject().contains("Forgot username on"));
receivedMailContent = getMessageContent(receivedEmail).get("textPart");
assertTrue(receivedMailContent.contains(String.format("XWiki.%s", user1Login)));
assertTrue(receivedMailContent.contains(String.format("XWiki.%s", user2Login)));
}
}
Expand Up @@ -21,6 +21,7 @@

import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.xwiki.stability.Unstable;
import org.xwiki.test.ui.po.ViewPage;

/**
Expand All @@ -31,6 +32,10 @@
*/
public class ForgotUsernameCompletePage extends ViewPage
{
/**
* @deprecated since 12.10.5 and 13.2RC1 this message is no longer displayed.
*/
@Deprecated
public boolean isUsernameRetrieved(String username)
{
try {
Expand All @@ -42,8 +47,37 @@ public boolean isUsernameRetrieved(String username)
}
}

/**
* @deprecated since 12.10.5 and 13.2RC1 this message is no longer displayed.
*/
@Deprecated
public boolean isAccountNotFound()
{
return getContent().contains("No account is registered using this email address");
}

/**
* @return the text content of the message box.
* @since 12.10.5
* @since 13.2RC1
*/
@Unstable
public String getMessage()
{
return getContent();
}

/**
* @return {@code true} if the forgot username query was successfully sent (without any error).
* @since 12.10.5
* @since 13.2RC1
*/
@Unstable
public boolean isForgotUsernameQuerySent()
{
// If there is no form and we see an info box, then the request was sent.
return !getDriver().hasElementWithoutWaiting(By.cssSelector("#forgotUsernameForm"))
&& getMessage().contains("If an account is registered with this email, "
+ "you will receive the account information on");
}
}
Expand Up @@ -54,22 +54,64 @@
#if($results.size() == 0 &amp;&amp; ${xcontext.database} != ${xcontext.mainWikiName})
#set($results = $query.setWiki("${xcontext.mainWikiName}").execute())
#end
#if($results.size() == 0)
{{translation key="xe.admin.forgotUsername.error.noAccount"/}}
#set ($emailError = false)
#if($results.size() != 0)
## Send the email
#set ($from = $services.mail.sender.configuration.fromAddress)
#if ("$!from" == '')
#set ($from = "no-reply@${request.serverName}")
#end
## The mail template use $usernames to display the results.
#set ($usernames = $results)
#set ($mailTemplateReference = $services.model.createDocumentReference('', 'XWiki', 'ForgotUsernameMailContent'))
#set ($mailParameters = {'from' : $from, 'to' : $email, 'language' : $xcontext.locale})
#set ($message = $services.mail.sender.createMessage('template', $mailTemplateReference, $mailParameters))
#set ($discard = $message.setType('Forgot Username'))
#macro (displayError $text)

[[{{translation key="xe.admin.forgotUsername.error.retry"/}}&gt;&gt;$doc.fullName]] | [[{{translation key="xe.admin.forgotUsername.login"/}}&gt;&gt;path:${xwiki.getURL('XWiki.XWikiLogin', 'login')}]]
#elseif($results.size() == 1)
$services.localization.render('xe.admin.forgotUsername.result', ["**${results.get(0).substring($results.get(0).indexOf('.')).substring(1)}**"])
{{html}}
&lt;div class="xwikirenderingerror" title="Click to get more details about the error" style="cursor: pointer;"&gt;
$services.localization.render('xe.admin.forgotUsername.error.emailFailed')
&lt;/div&gt;
&lt;div class="xwikirenderingerrordescription hidden"&gt;
&lt;pre&gt;${text}&lt;/pre&gt;
&lt;/div&gt;
{{/html}}

[[{{translation key="xe.admin.forgotUsername.login"/}}&gt;&gt;path:${xwiki.getURL('XWiki.XWikiLogin', 'login')}]]
#else
{{translation key="xe.admin.forgotUsername.multipleResults"/}}
#foreach($item in $results)
* **${item.substring($item.indexOf('.')).substring(1)}**
#set ($emailError = true)
#end
## Check for an error constructing the message!
#if ($services.mail.sender.lastError)
#displayError($exceptiontool.getStackTrace($services.mail.sender.lastError))
#else
## Send the message and wait for it to be sent or for any error to be raised.
#set ($mailResult = $services.mail.sender.send([$message], 'database'))
## Check for errors during the send
#if ($services.mail.sender.lastError)
#displayError($exceptiontool.getStackTrace($services.mail.sender.lastError))
#else
#set ($failedMailStatuses = $mailResult.statusResult.getAllErrors())
#if ($failedMailStatuses.hasNext())
#set ($mailStatus = $failedMailStatuses.next())
#displayError($mailStatus.errorDescription)
#end
#end
#end
#end
## We always display a success message even if there's no user found to avoid disclosing information
## about the users registered on the wiki.
#if (!$emailError)
{{success}}
$services.localization.render('xe.admin.forgotUsername.emailSent', ["$email"])

[[{{translation key="xe.admin.forgotUsername.login"/}}&gt;&gt;path:${xwiki.getURL('XWiki.XWikiLogin', 'login')}]]
#end
{{html}}
&lt;div&gt;
&lt;a href="$xwiki.getURL('XWiki.XWikiLogin', 'login')"&gt;$services.localization.render('xe.admin.forgotUsername.login')&lt;/a&gt;
&lt;/div&gt;
{{/html}}

{{/success}}
#end
#end
{{/velocity}}</content>
<object>
Expand Down

0 comments on commit f0440df

Please sign in to comment.