WinstoneでOpenID4Java

 openid4javaライブラリは、自前でディスカバリを走らせるのでロジックだけのテストができない。しょうがないのでWinstoneを使って、JUnitからテスト前にOPを起動させてみた。*1


leathersole's openid4javatest at master - GitHub
http://github.com/leathersole/openid4javatest


 eclipseで、自動ビルド先をプロジェクト/test-resources/WEB-INF/classes と指定して、OpMock#createProvider()から起動。


 OPとRPのコードは、サンプルのsample-openidを編集している。Winstonejspをつかうことすら面倒だったので、全部Servlet化。

public class OpMock {
	public void createProvider() throws IOException {
        Map<String, String> prop = new HashMap<String, String>();
        prop.put("webroot", "test-resources");
        prop.put("httpPort", "28080");
        Launcher.initLogger(prop);

        final Launcher winstone = new Launcher(prop);

        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            public void run() {
                winstone.shutdown();        
            }
        }));

	}
}


 今のテストのターゲットは、RPの内部ロジックなので、RPはまだWinstone起動の対象にはしていない。SpringMockHttpServletRequest/MockHttpServletResponseを使って、直にServletを呼び出している。とはいえ、簡単に起動できるだろう。


 テストコードは最後に載せるが、長い。アサーションが多くて、テスト独立性のポリシーにも反している。途中までのテストを分割することはできるが、OPでなされた認証をRPで検証するためには、この一連の処理が必要だろうなぁ。


 今回は、最初にOPが動くまで2時間程度。前Winstoneを試したときは、ちょっと制約が多い気がしてたけど、割り切ってしまえば簡単なものだな。


やるかわからない残タスク:
・ネガティブアサーション対応
・ax対応
sreg対応
・pape対応
・RPもWinstoneで動かす
・antビルド対応
・war作る
・hudsonのように、Winstone内蔵のwarにする


参考サイト:
Parsing query strings in Java - Stack Overflow
Final: OpenID Authentication 2.0 - 最終版
Winstone Servlet Container
軽量サーブレットコンテナ winstone を開発用にサクッと使う - etc9


テストコード:

public class ConsumerServletTest {
	/**
	 * ConsumerServletにて認証をテストする。
	 * 
	 * @throws Exception
	 */
	@Test
	public void authorizeTest() throws Exception {
		// OPを起動
		OpMock opMock = new OpMock();
		opMock.createProvider();
		// ConsumerServlet用のMockを生成
		MockServletConfig config = new MockServletConfig();
		MockHttpServletRequest request = new MockHttpServletRequest();
		MockHttpServletResponse response = new MockHttpServletResponse();
		// ConsumerServletをテスト
		ConsumerServlet servlet = new ConsumerServlet();
		servlet.init(config);

		request
				.addParameter("openid_identifier",
						"http://localhost:28080/xrds");
		servlet.doPost(request, response);

		AuthRequest authReq = servlet.getAuthReq();

		assertEquals("http://localhost:28080/provider", authReq.getOPEndpoint());
		assertEquals("checkid_setup", authReq.getParameterValue("openid.mode"));

		HttpSession session = request.getSession();
		Map parameterMap = authReq.getParameterMap();

		// OPにて認証を実行
		String returnTo = authorizeByOp(authReq, parameterMap);
		// ConsumerServletに戻る
		String returnToUri = returnTo.substring(0, returnTo.indexOf("?"));
		String returnToQueryString = returnTo.substring(
				returnTo.indexOf("?") + 1, returnTo.length());
		Map<String, String> queryMap = getQueryMap(returnToQueryString);

		MockHttpServletRequest request2 = new MockHttpServletRequest();
		MockHttpServletResponse response2 = new MockHttpServletResponse();

		for (String key : queryMap.keySet()) {
			request2.setParameter(key, URLDecoder.decode(queryMap.get(key),
					"UTF-8"));
		}
		request2.setSession(session);
		request2.setQueryString(returnToQueryString);
		servlet.doGet(request2, response2);
	}

	private String authorizeByOp(AuthRequest authReq, Map parameterMap)
			throws IOException, HttpException {
		HttpClient client = new HttpClient();
		PostMethod post = new PostMethod(authReq.getOPEndpoint());
		for (Object key : parameterMap.keySet()) {
			post.addParameter(key.toString(), parameterMap.get(key).toString());
		}
		int statusCode = client.executeMethod(post);
		assertEquals(302, statusCode);
		Header locationHeader = post.getResponseHeader("location");
		String location;
		location = locationHeader.getValue();
		assertEquals("http://localhost:28080/provider_authorization", location);

		GetMethod get = new GetMethod(location + "?action=authorize");
		get.setFollowRedirects(false);
		statusCode = client.executeMethod(get);
		assertEquals(302, statusCode);

		Header completeHeader = get.getResponseHeader("location");
		String complete = completeHeader.getValue();
		GetMethod get2 = new GetMethod(complete);
		get2.setFollowRedirects(false);
		statusCode = client.executeMethod(get2);

		Header authorizedHeader = get2.getResponseHeader("location");
		String returnTo = authorizedHeader.getValue();
		return returnTo;
	}

	public static Map<String, String> getQueryMap(String query) {
		String[] params = query.split("&");
		Map<String, String> map = new HashMap<String, String>();
		for (String param : params) {
			String name = param.split("=")[0];
			String value = param.split("=")[1];
			map.put(name, value);
		}
		return map;
	}
}

*1:かなり(仕事的にも)今更なネタではある。