| Issue 16: | RelyingParty and unit testing | |
| 1 person starred this issue and may be notified of changes. | Back to list |
Sign in to add a comment
|
David,
First, thanks for having made the code available to all of us. That's really cool and help saving a pretty
good amount of work.
I want to write unit tests for the LogoutServlet class:
public class LogoutServlet extends HttpServlet {
//...
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException,
ServletException {
RelyingParty relyingParty = getInstance(); // Externalize to allow mock injection
relyingParty.invalidate(request, response);
response.sendRedirect(ApplicationSettings.get().getMainPageURL());
}
//...
protected RelyingParty getInstance() {
return RelyingParty.getInstance();
}
}
One goal is to ensure the "invalidate()" method is called.
I've written the following code but it does not work because RelyingParty is a final class...
@Test
@SuppressWarnings("serial")
public void testInvalidate() throws IOException, ServletException {
HttpServletRequest request = new MockHttpServletRequest();
HttpServletResponse response = new MockHttpServletResponse() {
@Override
public void sendRedirect(String url) throws IOException {
assertEquals(ApplicationSettings.DEFAULT_MAIN_PAGE_URL, url);
}
};
final RelyingParty relyingParty = EasyMock.createMock(RelyingParty.class);
EasyMock.expect(relyingParty.invalidate(request, response));
EasyMock.replay();
new LogoutServlet() {
@Override
public RelyingParty getInstance() {
return new RelyingParty() {
};
}
}.doGet(request, response);
}
Do you have a suggestion?
For now, I just rely on the RelyingParty.getInstance() method. The test passes but I've no way to ensure that
future refactoring won't remove the call to "invalidate()" by mistake. This is not a safe situation,
especially in my Agile environment ;)
A+, Dom
|
||||||||||
,
Nov 03, 2009
The piece of code in the unit test method should be read as:
new LogoutServlet() {
@Override
public RelyingParty getInstance() {
return relyingParty; // The EasyMock instance
}
}.doGet(request, response);
|
|||||||||||
,
Nov 03, 2009
This is somewhat the negative effect for marking it final. The solution is not to mock the RelyingParty but instead mock the components that the RelyingParty is using. - OpenIdUserManager, UserCache, AuthRedirection The OpenIdContext is also marked final but its wrapped components can be mocked. - Association, Discovery, HttpConnector You can then instantiate the RelyingParty with the mocked components in the constructor. Would that work for you?
Owner: david.yu.ftw
|
|||||||||||
,
Nov 04, 2009
This is the avenue I've started to use ;) It does require the creation of a lot of mock classes and a good understanding of your code. Without it, this solution would not be possible and you would to code them for us. Once I've finished the coding, I'm going to propose you to review it. Stay tuned ;) A+, Dom |
|||||||||||
,
Nov 04, 2009
David,
Just a weird path I've identified while unit testing your HomeServlet code. It's
related to the following piece of code:
if (user.isAssociated() && RelyingParty.isAuthResponse(request)) {
// verify authentication
if (relyingParty.verifyAuth(user, request, response)) {
// authenticated redirect to home to remove the query params instead of
doing:
// request.setAttribute("user", user);
request.getRequestDispatcher("/home.jsp").forward(request, response);
response.sendRedirect(ApplicationSettings.get().getMainPageURL());
}
else {
// failed verification
request.getRequestDispatcher(ApplicationSettings.get().getLoginPageURL()).forward(request,
response);
}
return;
}
// associate and authenticate user
StringBuffer url = request.getRequestURL();
String trustRoot = url.substring(0, url.indexOf("/", 9));
String realm = url.substring(0, url.lastIndexOf("/"));
String returnTo = url.toString();
if (relyingParty.associateAndAuthenticate(user, request, response, trustRoot,
realm, returnTo)) {
// successful association
return;
}
The first "if(){}" block is skipped if the user is not associated (some attributes
missing or if the request comes back from the authentication service. With this block
skipped, there's a call to RelyingParty.associateAndAuthenticate() which goes up to
RelyingParty.getAuthUrlMap() which contains this test:
if(!user.isAssociated())
throw new IllegalArgumentException("claimed_id of user has not been verified.");
If my understanding is correct, it seems that RelyingParty.discover() can only
deliver null or an already associated user. It seems your code would work without the
test on user.isAssociated() in the first "if(){}" block.
Maybe the test on the association should be done explicitly after the call to
discover() to be more obvious (instead of embedding it in getAuthUrlMap()...
A+, Dom
Note: I'm not that far to provide good code coverage for your code with the help of
many mock classes. For now, only one have been instrumented to fake successful
authentication ;)
|
|||||||||||
,
Nov 04, 2009
RelyingParty.discovery(request) can either return null, an associated user, an authenticated user or a newly discovered (unassociated) user. Removing the if(user.isAssociated()) check would not work. The RelyingParty.isAuthResponse(request) can be mocked by simply appending "?openid.mode=id_res" to the url. So instead of associating the newly discovered user, he would instead get redirected back to the login page because the inner if(RelyingParty.verifyAuth(user,request,response) would fail. PS. At first I agreed with your approach ... you almost convinced me :-) Had to double-check. |
|||||||||||
,
Nov 04, 2009
FYI, here is the test verifying the invalidation, in the LogoutServlet, has been done
as expected:
@Test
@SuppressWarnings("serial")
public void testInvalidate() throws IOException, ServletException {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse() {
@Override
public void sendRedirect(String url) throws IOException {
assertEquals(ApplicationSettings.DEFAULT_MAIN_PAGE_URL, url);
}
};
Properties propertiesForInjection = new Properties() {
propertiesForInjection.setProperty("openid.discovery",
"com.dyuproject.openid.MockDiscovery");
propertiesForInjection.setProperty("openid.association",
"com.dyuproject.openid.MockAssociation");
propertiesForInjection.setProperty("openid.httpconnector",
"com.dyuproject.util.http.MockHttpConnector");
propertiesForInjection.setProperty("openid.user.manager",
"com.dyuproject.openid.MockOpenIdUserManager");
propertiesForInjection.setProperty("openid.user.cache",
"com.dyuproject.openid.MockUserCache");
propertiesForInjection.setProperty("openid.authredirection",
"com.dyuproject.openid.MockAuthRedirection");
};
final RelyingParty relyingParty =
RelyingParty.newInstance(propertiesForInjection);;
new LogoutServlet() {
@Override
protected RelyingParty getRelyingParty() {
return relyingParty;
}
}.doGet(request, response);
assertTrue(((MockOpenIdUserManager)
relyingParty.getOpenIdUserManager()).isInvalidated());
}
I had to create a bunch of mock classes but that works fine (the ones I use a lot are
on
http://github.com/DomDerrien/two-tiers-utils/tree/master/src/Java/domderrien/mocks/).
I'm going to post the mock files, my two servlet files, and my test files on GitHub
in few days. You'll be able to package them with the rest of your files ;)
As far as this defect entry is concerned, there's no more issue.
You can close it.
A+, Dom
|
|||||||||||
,
Nov 04, 2009
Thanks for the investigation around the OpenIdUser.isAssociated(). I think I understand your point but I cannot create the corresponding test case... You'll probably be able to set one up quickly with the code I'm going to publish in few days. Thank you very much, A+, Dom |
|||||||||||
,
Nov 20, 2009
Hi David, Here are the code I would like you to validate ;) Note that I cannot exercise the path you mentioned. If you don't mind it will be great to produce the exact test case for it. First, there is the Login servlet code, and then its corresponding unit test suite. The mock class I created (most of them are simple implementation of your interfaces) are available on my open source project on http://github.com/DomDerrien/two-tiers-utils/tree/b8ef96bd09a684651cd4164c1fe6abf6c64ef419/src/Java/com/dyuproject. Feel free to reuse these classes into your project. IMO, it's always nice to offer mock classes along with the code, so third parties can write accurate tests ;) A+, Dom Note: because one post with the code is too long, I'm going to submit it in many parts... |
|||||||||||
,
Nov 20, 2009
The login servlet code --****************************************************************-- package domderrien.j2ee; // Adaptation of David Yu's code for dyuproject, made available under // the Apache licence 2.0. The adaptation mostly introduce more flexibility // regarding the workflow and to make it unit-test-able package domderrien.j2ee; import java.io.FileNotFoundException; import java.io.IOException; import java.net.UnknownHostException; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.dyuproject.openid.OpenIdServletFilter; import com.dyuproject.openid.OpenIdUser; import com.dyuproject.openid.RelyingParty; import com.dyuproject.openid.YadisDiscovery; import com.dyuproject.openid.RelyingParty.Listener; import com.dyuproject.openid.ext.AxSchemaExtension; import com.dyuproject.openid.ext.SRegExtension; import com.dyuproject.util.http.UrlEncodedParameterMap; /** * Home Servlet. If authenticated, goes to the home page. If not, goes to the login page. * * @author David Yu * @maintainer Dom Derrien */ @SuppressWarnings("serial") public class LoginServlet extends HttpServlet { protected static Listener sregExtension = new SRegExtension().addExchange("email").addExchange("country").addExchange("language"); protected static Listener axSchemaExtension = new AxSchemaExtension().addExchange("email").addExchange("country").addExchange("language"); protected static Listener relyingPartyListener = new RelyingParty.Listener() { public void onDiscovery(OpenIdUser user, HttpServletRequest request) { System.err.println("******** discovered user: " + user.getClaimedId()); } public void onPreAuthenticate(OpenIdUser user, HttpServletRequest request, UrlEncodedParameterMap params) { System.err.println("******** pre-authenticate user: " + user.getClaimedId()); } public void onAuthenticate(OpenIdUser user, HttpServletRequest request) { System.err.println("******** newly authenticated user: " + user.getIdentity()); Map<String, String> sreg = SRegExtension.remove(user); Map<String, String> axschema = AxSchemaExtension.remove(user); if (sreg != null && !sreg.isEmpty()) { user.setAttribute("info", sreg); } else if (axschema != null && !axschema.isEmpty()) { user.setAttribute("info", axschema); } } public void onAccess(OpenIdUser user, HttpServletRequest request) { System.err.println("******** user access: " + user.getIdentity()); System.err.println("******** info: " + user.getAttribute("info")); } }; protected static RelyingParty _relyingParty; static { _relyingParty = RelyingParty.getInstance(). addListener(sregExtension). addListener(axSchemaExtension). addListener(relyingPartyListener); } // To allow injection of a mock instance protected RelyingParty getRelyingParty() { return _relyingParty; } @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { doPost(request, response); } public final static String LOGIN_WITH_PARAMETER_KEY = "loginWith"; public final static String GOOGLE_OPENID_SERVER_URL = "https://www.google.com/accounts/o8/ud"; public final static String YAHOO_OPENID_SERVER_URL = "https://open.login.yahooapis.com/openid/op/auth"; protected void preselectOpendIdServer(HttpServletRequest request) { String identifier = null; String openIdServer = null; // If the ui supplies a LoginWith=google or LoginWith=yahoo link/button, // this will speed up the openid process by skipping discovery. // The override is done by adding the OpenIdUser to the request attribute. String loginWith = request.getParameter(LOGIN_WITH_PARAMETER_KEY); String openidIdentifier = request.getParameter(RelyingParty.DEFAULT_IDENTIFIER_PARAMETER); if (loginWith != null) { if (loginWith.equals("google")) { identifier = "https://www.google.com/accounts/o8/id"; openIdServer = GOOGLE_OPENID_SERVER_URL; } else if (loginWith.equals("yahoo")) { identifier = "http://yahoo.com/"; openIdServer = YAHOO_OPENID_SERVER_URL; } } else if (openidIdentifier != null) { if (openidIdentifier.contains("@gmail")) { identifier = "https://www.google.com/accounts/o8/id"; openIdServer = GOOGLE_OPENID_SERVER_URL; } if (openidIdentifier.contains("@yahoo")) { identifier = "http://yahoo.com/"; openIdServer = YAHOO_OPENID_SERVER_URL; } } if (identifier != null) { // && openIdServer != null) { request.setAttribute( OpenIdUser.ATTR_NAME, OpenIdUser.populate( identifier, YadisDiscovery.IDENTIFIER_SELECT, // claimedId, openIdServer ) ); } } @Override public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { preselectOpendIdServer(request); RelyingParty relyingParty = getRelyingParty(); String errorMsg = OpenIdServletFilter.DEFAULT_ERROR_MSG; try { OpenIdUser user = relyingParty.discover(request); if (user == null) { if (RelyingParty.isAuthResponse(request)) { // authentication timeout response.sendRedirect(request.getRequestURI()); } else { // set error msg if the openid_identifier is not resolved. if (request.getParameter(relyingParty.getIdentifierParameter()) != null) { request.setAttribute(OpenIdServletFilter.ERROR_MSG_ATTR, errorMsg); } // new user request.getRequestDispatcher("/login").forward(request, response); } return; } if (user.isAuthenticated()) { // user already authenticated request.getRequestDispatcher("/login").forward(request, response); return; } // if (user.isAssociated() && RelyingParty.isAuthResponse(request)) { // Temporary shortcut, waiting for David Yu's response if (RelyingParty.isAuthResponse(request)) { // verify authentication if (relyingParty.verifyAuth(user, request, response)) { // authenticated redirect to home to remove the query params instead of doing: // request.setAttribute("user", user); request.getRequestDispatcher("/home.jsp").forward(request, response); response.sendRedirect("/login"); } else { // failed verification request.getRequestDispatcher("/login").forward(request, response); } return; } // associate and authenticate user StringBuffer url = request.getRequestURL(); String trustRoot = url.substring(0, url.indexOf("/", 9)); String realm = url.substring(0, url.lastIndexOf("/")); String returnTo = url.toString(); if (relyingParty.associateAndAuthenticate(user, request, response, trustRoot, realm, returnTo)) { // successful association return; } } catch (UnknownHostException uhe) { errorMsg = OpenIdServletFilter.ID_NOT_FOUND_MSG; } catch (FileNotFoundException fnfe) { errorMsg = OpenIdServletFilter.DEFAULT_ERROR_MSG; } catch (Exception e) { // e.printStackTrace(); errorMsg = OpenIdServletFilter.DEFAULT_ERROR_MSG; } request.setAttribute(OpenIdServletFilter.ERROR_MSG_ATTR, errorMsg); request.getRequestDispatcher("/login").forward(request, response); } } |
|||||||||||
,
Nov 20, 2009
The test class attached |
|||||||||||
| ► Sign in to add a comment | |||||||||||