Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

EQC tests and a simple eunit test to run them. #11

Merged
merged 7 commits into from Feb 28, 2012
@@ -87,35 +87,35 @@ https_redirect (RD,Ctx) ->
%% - `none' :: No authentication.
%%
enforce_auth(RD, Ctx) ->
- case wrq:get_req_header("authorization", RD) of
- "Basic "++Base64 ->
- enforce_basic_auth(RD, Ctx, Base64);
- _ ->
- {?ADMIN_AUTH_HEAD, RD, Ctx}
+ case app_helper:get_env(riak_control,auth,none) of
+ none ->
+ {true, RD, Ctx};
+ Auth ->
+ case wrq:get_req_header("authorization", RD) of
+ "Basic "++Base64 ->
+ enforce_basic_auth(RD, Ctx, Base64, Auth);
+ _ ->
+ {?ADMIN_AUTH_HEAD, RD, Ctx}
+ end
end.
-enforce_basic_auth(RD, Ctx, Base64) ->
+enforce_basic_auth(RD, Ctx, Base64, Auth) ->
Str = base64:mime_decode_to_string(Base64),
case string:tokens(Str, ":") of
[User, Pass] ->
- enforce_user_pass(RD, Ctx, User, Pass);
+ enforce_user_pass(RD, Ctx, User, Pass, Auth);
_ ->
{?ADMIN_AUTH_HEAD, RD, Ctx}
end.
-enforce_user_pass(RD, Ctx, User, Pass) ->
- case valid_userpass(User, Pass) of
+enforce_user_pass(RD, Ctx, User, Pass, Auth) ->
+ case valid_userpass(User, Pass, Auth) of
true ->
{true, RD, Ctx};
false ->
{?ADMIN_AUTH_HEAD, RD, Ctx}
end.
-%% validate the username and password
-valid_userpass(User, Pass) ->
- Auth=app_helper:get_env(riak_control, auth),
- valid_userpass(User, Pass, Auth).
-
%% validate the username and password with the given auth style
valid_userpass(_User, _Pass, none) ->
true;
@@ -21,6 +21,10 @@
-module(riak_control_session).
-behavior(gen_server).
+-ifdef (TEST).
+-include_lib("eunit/include/eunit.hrl").
+-endif.
+
%% API
-export([start_link/0,
get_version/0,
@@ -321,3 +325,23 @@ get_vnode_status (Service,Ring,Index) ->
[] -> {Service,undefined}
end.
+
+%% ---------------------------------------------------------------------------
+%% EUNIT tests
+
+-ifdef(TEST).
+
+%% we want to see quickcheck output
+-define(QC_OUT(P),
+ eqc:on_output(fun(Str, Args) ->
+ io:format(user, Str, Args)
+ end,
+ P)).
+
+quickcheck_test_ () ->
+ {timeout, 120,
+ fun() ->
+ ?assert(eqc:quickcheck(?QC_OUT(eqc_routes:prop_routes())))
+ end}.
+
+-endif.
View
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICvTCCAiYCCQDgxT3HogRJ/TANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMC
+VVMxFjAUBgNVBAgTDU1hc3NhY2h1c2V0dHMxEjAQBgNVBAcTCUNhbWJyaWRnZTEf
+MB0GA1UEChMWQmFzaG8gVGVjaG5vbG9naWVzIEluYzENMAsGA1UECxMEUmlhazEY
+MBYGA1UEAxMPSmVmZnJleSBNYXNzdW5nMR0wGwYJKoZIhvcNAQkBFg5qZWZmQGJh
+c2hvLmNvbTAeFw0xMTEwMzExNjQ3NTNaFw0yMTEwMjgxNjQ3NTNaMIGiMQswCQYD
+VQQGEwJVUzEWMBQGA1UECBMNTWFzc2FjaHVzZXR0czESMBAGA1UEBxMJQ2FtYnJp
+ZGdlMR8wHQYDVQQKExZCYXNobyBUZWNobm9sb2dpZXMgSW5jMQ0wCwYDVQQLEwRS
+aWFrMRgwFgYDVQQDEw9KZWZmcmV5IE1hc3N1bmcxHTAbBgkqhkiG9w0BCQEWDmpl
+ZmZAYmFzaG8uY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCkys/CB8Ce
+fO19JsFiL2K5pODMWFmXxfQgvARB2rIvoJ4R4mNKI639xRbR+gCPreJvZRw8trgD
+8sARFv8J1SlqqYRDN8zfMlvolXh6Atujou/LwjUpTA3pMe9lZrWU1+JZOMAk79lz
+O/1etfR12By0SqNfDgUMIIZST7i3Fw2IkwIDAQABMA0GCSqGSIb3DQEBBQUAA4GB
+ABTOXMBYoEn0biwqaDcZzInZvZHupFkmkOABumgJ5xVDQ/9LIzy9mfg0Ko+JERlM
+0w0dliu2sfFoXLps9EohjIzrU1B3CwSNvNqPmcj9V4k0iXrsvDfbG9eJ9nYaUY0Y
+L5I9/KAOIf3fEmnFbjtmyiLVhrM6kBB3fvoAVQfwL6cZ
+-----END CERTIFICATE-----
View
@@ -0,0 +1,243 @@
+-module(eqc_routes).
+-compile(export_all).
+
+-ifdef(EQC).
+
+%% EQC headers
+-include_lib("eqc/include/eqc.hrl").
+-include_lib("eqc/include/eqc_statem.hrl").
+
+%% command generators
+-define(CLUSTER_LIST,{call,?MODULE,cluster_list,[]}).
+-define(PARTITION_LIST,{call,?MODULE,partition_list,[]}).
+-define(HTTP_CLUSTER_LIST(U),{call,?MODULE,http_cluster_list,[U]}).
+-define(HTTP_PARTITION_LIST(U,F),{call,?MODULE,http_partition_list,[U,F]}).
+
+
+%% ---------------------------------------------------------------------------
+%% statem state
+
+-record(state,{auth,ring_size}).
+
+
+%% ---------------------------------------------------------------------------
+%% properties
+
+prop_routes () ->
+ ?FORALL(Auth,g_auth(),
+ collect(Auth,
+ begin
+ setup(Auth),
+
+ %% start application dependencies
+ riak_core_util:start_app_deps(riak_core),
+ riak_core_util:start_app_deps(riak_control),
+
+ %% can now start riak control
+ application:start(riak_control),
+
+ %% ensure riak_control is up and running
+ riak_core:wait_for_application(riak_control),
+ {ok,Size}=application:get_env(riak_core,ring_creation_size),
+
+ %% now run the test with these auth settings
+ ?ALWAYS(10,
+ ?FORALL(Cmds,commands(?MODULE,#state{auth=Auth,
+ ring_size=Size
+ }),
+ begin
+ {H,S,Res}=run_commands(?MODULE,Cmds),
+
+ %% results
+ ?WHENFAIL(
+ io:format("H: ~p\nS: ~p\nR: ~p\n",
+ [H,S,Res]),
+ Res==ok)
+ end))
+ end)).
+
+
+%% ---------------------------------------------------------------------------
+%% statem behavior
+
+initial_state () ->
+ #state{}.
+
+command (_State) ->
+ oneof([?CLUSTER_LIST,
+ ?PARTITION_LIST,
+ ?LET(User,g_userpass(),
+ oneof([?HTTP_CLUSTER_LIST(User),
+ ?LET(Filter,g_qs(),
+ ?HTTP_PARTITION_LIST(User,Filter))
+ ]))
+ ]).
+
+precondition (_State,_Gen) ->
+ true.
+
+postcondition (State,{call,_,http_cluster_list,[User]},Res) ->
+ http_validate(State,Res,User) andalso cluster_validate(State,Res);
+postcondition (State,{call,_,http_partition_list,[User,Filter]},Res) ->
+ http_validate(State,Res,User) andalso filter_validate(State,Res,Filter);
+postcondition (_State,_Gen,_Res) ->
+ true.
+
+next_state (State,_V,_Gen) ->
+ State.
+
+
+%% ---------------------------------------------------------------------------
+%% negative testing common post-conditions
+
+http_validate (#state{auth=userlist},{Code,_,_},{user,pass}) ->
+ Code < 300;
+http_validate (#state{auth=userlist},{Code,_,_},_) ->
+ Code >= 300;
+http_validate (#state{auth=none},{Code,_,_},_) ->
+ Code < 300.
+
+
+cluster_validate (_State,{Code,"",_Url}) ->
+ not (Code < 300);
+cluster_validate (_State,{_Code,Body,_Url}) ->
+ [{struct,Node}]=mochijson2:decode(Body),
+ {_,Name}=lists:keyfind(<<"name">>,1,Node),
+ {_,Me}=lists:keyfind(<<"me">>,1,Node),
+ {_,Reachable}=lists:keyfind(<<"reachable">>,1,Node),
+ binary_to_atom(Name,utf8) == node() andalso Me andalso Reachable.
+
+
+filter_validate (_State,{Code,"",_Url},_Filter) ->
+ not (Code < 300);
+filter_validate (#state{ring_size=Size},{_Code,Body,_Url},Filter) ->
+ {struct,Json}=mochijson2:decode(Body),
+ {_,Pages}=lists:keyfind(<<"pages">>,1,Json),
+ {_,Page}=lists:keyfind(<<"page">>,1,Json),
+ {_,Contents}=lists:keyfind(<<"contents">>,1,Json),
+ filter_validate_n(Size,Pages,Page,Contents,Filter) andalso
+ %% TODO: more validation here
+ true.
+
+
+filter_validate_n (Size,Pages,_Page,Contents,Filter) ->
+ ContentSize=erlang:length(Contents),
+ N=case lists:keyfind(n,1,Filter) of
+ false -> Size div Pages;
+ {_,X} -> X
+ end,
+ %% less than is okay for now, we might have returned the last page
+ ContentSize =< max(N,16).
+
+
+%% ---------------------------------------------------------------------------
+%% eqc generators
+
+%% valid authorization modes for app.config
+g_auth () ->
+ oneof([userlist,none]).
+
+%% username and password pairs (none = don't pass user/pass)
+g_userpass () ->
+ oneof([{user,pass},
+ {user,bad_password},
+ {bad_user,pass},
+ {bad_user,bad_password},
+ none % not passing auth data
+ ]).
+
+%% query string generator
+g_qs () ->
+ ?LET(Filter,g_filter(),
+ ?LET(Page,g_page(),
+ Filter ++ Page)).
+
+%% valid and invalid query string filters for ring page
+g_filter () ->
+ oneof([[{filter,node}],
+ [{filter,node},{q,g_node()}],
+ [{q,g_node()}],
+ [{filter,handoffs}],
+ [{filter,fallbacks}],
+ []
+ ]).
+
+%% pagination data for ring page
+g_page () ->
+ oneof([[{n,int()},{p,int()}],
+ [{n,int()}],
+ [{p,int()}],
+ []
+ ]).
+
+%% this node or a dummy node
+g_node () ->
+ oneof([node(),'dummy@nohost']).
+
+
+%% ---------------------------------------------------------------------------
+%% private
+
+f (Fmt,Args) ->
+ lists:flatten(io_lib:format(Fmt,Args)).
+
+set (K,V) ->
+ application:set_env(riak_core,K,V).
+
+setup (Auth) ->
+ application:load(riak_core),
+ application:load(riak_control),
+
+ %% hide sasl output
+ application:set_env(sasl,sasl_error_logger,false),
+
+ %% local the pem files in the test folder
+ PrivDir=code:priv_dir(riak_control),
+ TestDir=filename:join([PrivDir,"..","test"]),
+ CertFile=filename:join([TestDir,"cert.pem"]),
+ KeyFile=filename:join([TestDir,"key.pem"]),
+
+ %% set env values for riak core
+ set(http, [{"127.0.0.1",18098}]),
+ set(https, [{"127.0.0.1",18069}]),
+ set(ssl, [{certfile, CertFile},
+ {keyfile, KeyFile}
+ ]),
+
+ %% finally, enable riak control
+ application:set_env(riak_control,userlist,[{"user","pass"}]),
+ application:set_env(riak_control,enabled,true),
+ application:set_env(riak_control,admin,true),
+ application:set_env(riak_control,auth,Auth).
+
+cluster_list () ->
+ {ok,_V,Nodes}=riak_control_session:get_nodes(),
+ Nodes.
+
+partition_list () ->
+ {ok,_V,Ring}=riak_control_session:get_ring(),
+ Ring.
+
+http_cluster_list (User) ->
+ http_request(User,"/admin/cluster/list",[]).
+
+http_partition_list (User,Filter) ->
+ http_request(User,"/admin/ring/partitions",Filter).
+
+http_request (User,Route,Query) ->
+ Url=request_url(User,Route,Query),
+ case httpc:request(get,{Url,[]},[],[]) of
+ {ok,{{_HTTP,Code,_OK},_Headers,Body}} -> {Code,Body,Url};
+ {ok,{{_HTTP,Code,_OK},Body}} -> {Code,Body,Url}
+ end.
+
+request_url (User,Route,Query) ->
+ f("https://~slocalhost:18069~s?~s",[auth_user(User),Route,qs(Query)]).
+
+auth_user ({User,Pass}) -> f("~s:~s@",[User,Pass]);
+auth_user (_) -> "".
+
+qs ([{K,V}|QS]) -> f("~s=~w&~s", [K,V,qs(QS)]);
+qs ([]) -> "".
+
+-endif. % EQC
View
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQCkys/CB8CefO19JsFiL2K5pODMWFmXxfQgvARB2rIvoJ4R4mNK
+I639xRbR+gCPreJvZRw8trgD8sARFv8J1SlqqYRDN8zfMlvolXh6Atujou/LwjUp
+TA3pMe9lZrWU1+JZOMAk79lzO/1etfR12By0SqNfDgUMIIZST7i3Fw2IkwIDAQAB
+AoGANAW2eolZ/G5xxo2ChQ1yfCqZsMi/V9NtExxnt6Zjk/d/jyPJtnD3D2K1ponm
+vXTmQ8ZGmMAR7WUnzv1Ue/UoAntcyXwKAm+T+2IUJiir/qzYKLSn8FJ3wA+OWYKs
+1nSryi54IuNenKUslxMPDPk/0bM6nZS2AvbNPYhX7a8evXkCQQDNdsvDO3Ofn0pJ
++3bMLH5Ch/adrJ0TfF1H8n9pxiuq813ppffXsyzv3haTbGHOtEM5tILYbHmO16h1
+vY+hhoHHAkEAzVMVRaDefjp1qKfoyRm9ySa3GgH71t1dvm1jGTRxNk9M8pekLDz+
+GWyTzffM2/+8Xz4RFzLjqAoAjzBGMeAC1QJAANiycjV2fnvbhH6CuMieJIwG2hNx
++jiS8c7v83GbkHK8OlAyuzLDxqE1mpnhtUZM2JoDx/x6a7o7uXB0fQfe1QJBAKxi
+d/aYhJS4IjaymqfUm9m5TntgdP9FpcIOdugfdnmhhLochLK7lp7j4QhJZ07B3Hae
+Vp0Clc5sb2HIpvaS2+0CQEV5NxPjavmlCQksQvU2OAQvTW3Sm9lahTl4XvdVxfj0
+G9sZ2erg7MIo2LF4V6FM6Hbfoj/FhAMOXlXUoUGs1uI=
+-----END RSA PRIVATE KEY-----