Skip to content

Commit 4e19e68

Browse files
committed
MB-67857: Basic unit tests for menelaus_cbauth
Adds unit tests covering menelaus_cbauth's interactions with the json_rpc_connection workers. Change-Id: I087bb68068ce46f2d7c2ba20817b810f71fe165a Reviewed-on: https://review.couchbase.org/c/ns_server/+/232216 Reviewed-by: Timofey Barmin <[email protected]> Well-Formed: Restriction Checker Well-Formed: Build Bot <[email protected]> Tested-by: Peter Searby <[email protected]>
1 parent d971538 commit 4e19e68

File tree

4 files changed

+265
-9
lines changed

4 files changed

+265
-9
lines changed

src/menelaus_cbauth.erl

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@
3737
-include("ns_common.hrl").
3838
-include("cut.hrl").
3939

40+
-ifdef(TEST).
41+
-include_lib("eunit/include/eunit.hrl").
42+
-endif.
43+
4044
-define(VERSION_1, "v1").
45+
-define(WORKER_SYNC_TIMEOUT, 5000).
4146

4247
handle_rpc_connect(?VERSION_1, Label, Req) ->
4348
case ns_config_auth:is_system_provisioned() of
@@ -76,6 +81,9 @@ sync() ->
7681
sync(node()).
7782

7883
sync(Node) ->
84+
sync(Node, ?WORKER_SYNC_TIMEOUT).
85+
86+
sync(Node, WorkerSyncTimeout) ->
7987
InternalConnections =
8088
gen_server:call({?MODULE, Node}, get_internal_connections, infinity),
8189
%% If the above call succeeds, but a worker exits before we make the below
@@ -86,7 +94,7 @@ sync(Node) ->
8694
%% caller
8795
misc:parallel_map(
8896
fun (Pid) ->
89-
try menelaus_cbauth_worker:sync(Pid)
97+
try menelaus_cbauth_worker:sync(Pid, WorkerSyncTimeout)
9098
catch exit:{noproc, _} ->
9199
?log_error("Process ~p no longer exists", [Pid])
92100
end
@@ -639,3 +647,182 @@ service_to_label(xdcr) ->
639647
"goxdcr-cbauth";
640648
service_to_label(Service) ->
641649
atom_to_list(Service) ++ "-cbauth".
650+
651+
-ifdef(TEST).
652+
-define(SERVICE, "<service>").
653+
-define(LABEL, "<service>-cbauth").
654+
-define(HEARTBEAT_TIME_S, 1).
655+
%% Short timeout for waiting to confirm that something doesn't immediately
656+
%% happen, without waiting so long that it significantly increases test duration
657+
-define(SHORT_TIMEOUT, 500).
658+
%% Timeout for things that should eventually happen, but not necessarily
659+
%% immediately
660+
-define(LONG_TIMEOUT, 60_000).
661+
662+
setup_t() ->
663+
meck:expect(config_profile, get,
664+
fun () ->
665+
?DEFAULT_EMPTY_PROFILE_FOR_TESTS
666+
end),
667+
668+
fake_ns_config:setup(),
669+
fake_chronicle_kv:setup(),
670+
%% Test setups return a map of pids for later shutdown in the teardown
671+
PidMap = mock_helpers:setup_mocks([json_rpc_events,
672+
ns_node_disco_events,
673+
user_storage_events,
674+
ssl_service_events,
675+
json_rpc_connection_sup,
676+
ns_ssl_services_setup,
677+
menelaus_users,
678+
ns_secrets,
679+
testconditions]),
680+
681+
%% Set config values for a few keys, since these are needed for greater
682+
%% coverage, and to avoid errors
683+
fake_chronicle_kv:update_snapshot(#{nodes_wanted => [node()],
684+
bucket_names => []}),
685+
fake_ns_config:update_snapshot([{rest, [{port, 8091}]},
686+
{rest_creds, placeholder},
687+
{memcached, [{admin_user, "user"},
688+
{admin_pass, "pass"}]}]),
689+
690+
meck:new(menelaus_cbauth_worker, [passthrough]),
691+
692+
{ok, Pid} = menelaus_cbauth:start_link(),
693+
start_fake_json_rpc_connection(?LABEL),
694+
PidMap#{?MODULE => Pid}.
695+
696+
teardown_t(PidMap) ->
697+
Name = list_to_atom("json_rpc_connection-" ++ ?LABEL),
698+
erlang:unregister(Name),
699+
mock_helpers:shutdown_processes(PidMap),
700+
fake_chronicle_kv:teardown(),
701+
fake_ns_config:teardown(),
702+
meck:unload().
703+
704+
start_fake_json_rpc_connection(Label) ->
705+
Name = list_to_atom("json_rpc_connection-" ++ Label),
706+
Pid = self(),
707+
true = erlang:register(Name, Pid),
708+
meck:expect(json_rpc_connection, perform_call,
709+
fun (_Label, _Call, _EJsonArg, _Opts) ->
710+
{ok, true}
711+
end),
712+
gen_event:notify(json_rpc_events,
713+
{started, Label, [internal,
714+
{heartbeat, ?HEARTBEAT_TIME_S}],
715+
self()}).
716+
717+
cbauth_init_t() ->
718+
%% UpdateDB gets called once almost immediately
719+
meck:wait(json_rpc_connection, perform_call,
720+
['_', "AuthCacheSvc.UpdateDB", '_', '_'], ?LONG_TIMEOUT),
721+
%% Heartbeat gets called once
722+
meck:wait(json_rpc_connection, perform_call,
723+
['_', "AuthCacheSvc.Heartbeat", '_', '_'],
724+
2_000 * ?HEARTBEAT_TIME_S),
725+
%% Heartbeat gets called again
726+
meck:reset(json_rpc_connection),
727+
meck:wait(json_rpc_connection, perform_call,
728+
['_', "AuthCacheSvc.Heartbeat", '_', '_'],
729+
2_000 * ?HEARTBEAT_TIME_S),
730+
%% UpdateDB isn't called immediately again
731+
?assertError(timeout,
732+
meck:wait(json_rpc_connection, perform_call,
733+
['_', "AuthCacheSvc.UpdateDB", '_', '_'],
734+
?SHORT_TIMEOUT)).
735+
736+
cbauth_sync_t() ->
737+
%% UpdateDB gets called once immediately
738+
meck:wait(json_rpc_connection, perform_call,
739+
['_', "AuthCacheSvc.UpdateDB", '_', '_'],
740+
?LONG_TIMEOUT),
741+
[ok] = sync(),
742+
%% Ensure that the next perform_call doesn't immediately return until after
743+
%% the sync call would time out
744+
meck:expect(json_rpc_connection, perform_call,
745+
fun (_, "AuthCacheSvc.UpdateDB", _, _) ->
746+
timer:sleep(2 * ?SHORT_TIMEOUT);
747+
(_, _, _, _) ->
748+
ok
749+
end),
750+
meck:reset(menelaus_cbauth_worker),
751+
%% Force an update by updating the snapshot
752+
fake_ns_config:update_snapshot(rest, [{port, 8092}]),
753+
%% Wait for the update to start being handled
754+
meck:wait(menelaus_cbauth_worker, notify, ['_', '_'], ?LONG_TIMEOUT),
755+
%% Sync with timeout half the perform_call sleep time, to ensure it gets hit
756+
?assertExit({timeout, _}, sync(node(), ?SHORT_TIMEOUT)).
757+
758+
cbauth_stats_t() ->
759+
meck:expect(json_rpc_connection, perform_call,
760+
fun (_Label, "AuthCacheSvc.GetStats", _EJsonArg, _Opts) ->
761+
{ok, {[ok]}};
762+
(_Label, _Call, _EJsonArg, _Opts) ->
763+
{ok, true}
764+
end),
765+
[{<<?SERVICE>>, ok}] = stats(),
766+
meck:expect(json_rpc_connection, perform_call,
767+
fun (_Label, "AuthCacheSvc.GetStats", _EJsonArg, _Opts) ->
768+
{error, error};
769+
(_Label, _Call, _EJsonArg, _Opts) ->
770+
{ok, true}
771+
end),
772+
[] = stats().
773+
774+
trigger_notification(ns_node_disco_events) ->
775+
%% To avoid needing to trick ns_node_disco into recognising fake nodes, just
776+
%% make the node list different by removing the only node.
777+
%% Note, this tests ns_node_disco_events as well as chronicle_kv, as the
778+
%% event handler for chronicle_compat_events in this module does not cover
779+
%% the nodes_wanted key (although the ns_node_disco handler does cover this
780+
%% key, hence how this tests the ns_node_disco_events handler)
781+
fake_chronicle_kv:update_snapshot(nodes_wanted, []);
782+
trigger_notification(ns_config_events) ->
783+
%% This is just an arbitrary key in ns_config that we use for the cbauth
784+
%% info, that is covered by the chronicle_compat_events handler
785+
fake_ns_config:update_snapshot(rest, [{port, 8092}]);
786+
trigger_notification(chronicle_kv) ->
787+
%% While the bucket_names key isn't subscribed to, the collections key is,
788+
%% and we need to update both for the info to be updated
789+
fake_chronicle_kv:update_snapshot(
790+
#{bucket_names => ["test"],
791+
{bucket, "test", collections} => [{uid, 0}],
792+
{bucket, "test", props} => [{type, membase}]});
793+
trigger_notification(user_storage_events) ->
794+
meck:expect(menelaus_users, get_users_version, 0, {1, 0}),
795+
%% It isn't worth the complexity to trigger a user version change through
796+
%% less artificial means, so just manually trigger the event
797+
gen_event:notify(user_storage_events, event);
798+
trigger_notification(ssl_service_events) ->
799+
%% It isn't worth the complexity to trigger an ssl_service_event through
800+
%% less artificial means, so just manually trigger the event
801+
gen_event:notify(ssl_service_events, client_cert_changed).
802+
803+
cbauth_notify_tests() ->
804+
%% Check that expected events cause notifications
805+
[{"cbauth notify " ++ atom_to_list(EventManager) ++ " test",
806+
fun () ->
807+
%% UpdateDB gets called once almost immediately
808+
meck:wait(json_rpc_connection, perform_call,
809+
['_', "AuthCacheSvc.UpdateDB", '_', '_'],
810+
?LONG_TIMEOUT),
811+
meck:reset(json_rpc_connection),
812+
trigger_notification(EventManager),
813+
meck:wait(json_rpc_connection, perform_call,
814+
['_', "AuthCacheSvc.UpdateDB", '_', '_'],
815+
?LONG_TIMEOUT)
816+
end} || EventManager <- [ns_node_disco_events,
817+
ns_config_events,
818+
chronicle_kv,
819+
user_storage_events,
820+
ssl_service_events]].
821+
822+
cbauth_test_() ->
823+
{foreach, fun setup_t/0, fun teardown_t/1,
824+
[{"cbauth init test", fun cbauth_init_t/0},
825+
{"cbauth sync test", fun cbauth_sync_t/0},
826+
{"cbauth stats test", fun cbauth_stats_t/0}
827+
| cbauth_notify_tests()]}.
828+
-endif.

src/menelaus_cbauth_worker.erl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
-behaviour(gen_server).
1414

15-
-export([start_monitor/4, notify/2, collect_stats/1, sync/1,
15+
-export([start_monitor/4, notify/2, collect_stats/1, sync/2,
1616
strip_cbauth_suffix/1]).
1717

1818
-export([init/1, handle_call/3, handle_cast/2,
@@ -50,8 +50,8 @@ notify(Pid, Info) ->
5050
collect_stats(Pid) ->
5151
gen_server:call(Pid, collect_stats).
5252

53-
sync(Pid) ->
54-
gen_server:call(Pid, sync).
53+
sync(Pid, Timeout) ->
54+
gen_server:call(Pid, sync, Timeout).
5555

5656
init([Label, Version, Pid, Params]) ->
5757
MRef = erlang:monitor(process, Pid),

test/fake_helpers/fake_ns_config.erl

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,18 @@ meck_setup() ->
137137
meck_setup_getters(),
138138
meck_setup_setters(),
139139

140-
%% This function is slightly interesting. It uses some "fold" function in
141-
%% ns_config that requires the snapshot to be in a specific format.
140+
%% These functions are slightly interesting - they require the snapshot to
141+
%% be in a specific format.
142142
%% The snapshot format is trivial to map to, so rather than re-implement
143143
%% (simple as the logic is) just map the snapshot to the expected format
144144
%% and pass it through to the base function, which should stop this from
145145
%% ever getting out of sync.
146+
meck:expect(ns_config, fold,
147+
fun(Fun, Acc, ?NS_CONFIG_LATEST_MARKER) ->
148+
meck:passthrough([Fun, Acc, [get_ets_snapshot()]]);
149+
(Fun, Acc, Snapshot) ->
150+
meck:passthrough([Fun, Acc, [Snapshot]])
151+
end),
146152
meck:expect(ns_config, get_node_uuid_map,
147153
fun(?NS_CONFIG_LATEST_MARKER) ->
148154
meck:passthrough([[get_ets_snapshot()]]);
@@ -197,13 +203,20 @@ meck_setup_getters() ->
197203
end
198204
end),
199205

200-
206+
meck:expect(ns_config, search_node,
207+
fun(Snapshot, Key) ->
208+
fetch_node(node(), Snapshot, Key)
209+
end),
201210
meck:expect(ns_config, search_node,
202211
fun(Node, Snapshot, Key) ->
203212
fetch_node(Node, Snapshot, Key)
204213
end),
205214

206215

216+
meck:expect(ns_config, search_prop,
217+
fun (Snapshot, Key, SubKey) ->
218+
fetch_prop(Snapshot, Key, SubKey, undefined)
219+
end),
207220
meck:expect(ns_config, search_prop,
208221
fun(Snapshot, Key, SubKey, DefaultSubVal) ->
209222
fetch_prop(Snapshot, Key, SubKey, DefaultSubVal)
@@ -230,6 +243,10 @@ meck_setup_getters() ->
230243
meck:expect(ns_config, search_node_with_default,
231244
fun(Key, Default) ->
232245
fetch_with_default_from_latest_snapshot(Key, Default)
246+
end),
247+
meck:expect(ns_config, search_node_with_default,
248+
fun(Node, Snapshot, Key, Default) ->
249+
fetch_node_with_default(Node, Snapshot, Key, Default)
233250
end).
234251

235252
meck_setup_setters() ->
@@ -301,12 +318,20 @@ fetch_node_prop(Node, Snapshot, Key, SubKey, Default) ->
301318
false -> Default
302319
end.
303320

321+
fetch_node(Node, ?NS_CONFIG_LATEST_MARKER, Key) ->
322+
fetch_node(Node, get_ets_snapshot(), Key);
304323
fetch_node(Node, Snapshot, Key) ->
305324
case fetch_from_snapshot(Snapshot, {node, Node, Key}) of
306325
{value, _} = V -> V;
307326
false -> fetch_from_snapshot(Snapshot, Key)
308327
end.
309328

329+
fetch_node_with_default(Node, Snapshot, Key, Default) ->
330+
case fetch_from_snapshot(Snapshot, {node, Node, Key}) of
331+
{value, V} -> V;
332+
false -> fetch_with_default(Snapshot, Key, Default)
333+
end.
334+
310335
fetch_prop(?NS_CONFIG_LATEST_MARKER, Key, SubKey, DefaultSubVal) ->
311336
fetch_prop(get_ets_snapshot(), Key, SubKey, DefaultSubVal);
312337
fetch_prop(Snapshot, Key, SubKey, DefaultSubVal) ->

test/mock_helpers.erl

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,28 @@ compat_mode_events(PidMap) ->
150150
?FUNCTION_NAME}),
151151
PidMap#{?FUNCTION_NAME => CompatModeEventsPid}.
152152

153+
ns_node_disco(PidMap) ->
154+
setup_mocks([dist_manager], PidMap),
155+
{ok, NsNodeDiscoPid} = ns_node_disco:start_link(),
156+
PidMap#{?FUNCTION_NAME => NsNodeDiscoPid}.
157+
153158
ns_node_disco_events(PidMap) ->
159+
PidMap0 = setup_mocks([ns_node_disco], PidMap),
154160
{ok, NSNodeDiscoEventsPid} = gen_event:start_link({local,
155161
?FUNCTION_NAME}),
156-
PidMap#{?FUNCTION_NAME => NSNodeDiscoEventsPid}.
162+
PidMap0#{?FUNCTION_NAME => NSNodeDiscoEventsPid}.
163+
164+
json_rpc_events(PidMap) ->
165+
{ok, JsonRpcEventsPid} = gen_event:start_link({local, ?FUNCTION_NAME}),
166+
PidMap#{?FUNCTION_NAME => JsonRpcEventsPid}.
167+
168+
user_storage_events(PidMap) ->
169+
{ok, UserStorageEventsPid} = gen_event:start_link({local, ?FUNCTION_NAME}),
170+
PidMap#{?FUNCTION_NAME => UserStorageEventsPid}.
171+
172+
ssl_service_events(PidMap) ->
173+
{ok, SslServiceEventsPid} = gen_event:start_link({local, ?FUNCTION_NAME}),
174+
PidMap#{?FUNCTION_NAME => SslServiceEventsPid}.
157175

158176
%%%===================================================================
159177
%%% Mock setup functions
@@ -249,5 +267,31 @@ rebalance_quirks(PidMap) ->
249267

250268
testconditions(PidMap) ->
251269
meck:new(?FUNCTION_NAME, [passthrough]),
252-
meck:expect(?FUNCTION_NAME, get, fun(_) -> ok end),
270+
meck:expect(?FUNCTION_NAME, get, fun(_) -> false end),
271+
PidMap#{?FUNCTION_NAME => mocked}.
272+
273+
json_rpc_connection_sup(PidMap) ->
274+
meck:expect(?FUNCTION_NAME, reannounce, fun () -> ok end),
275+
PidMap#{?FUNCTION_NAME => mocked}.
276+
277+
ns_ssl_services_setup(PidMap) ->
278+
meck:expect(?FUNCTION_NAME, client_cert_auth, fun () -> [] end),
279+
PidMap#{?FUNCTION_NAME => mocked}.
280+
281+
menelaus_users(PidMap) ->
282+
meck:expect(?FUNCTION_NAME, get_auth_version,
283+
fun () -> {0, 0} end),
284+
meck:expect(?FUNCTION_NAME, get_users_version,
285+
fun () -> {0, 0} end),
286+
meck:expect(?FUNCTION_NAME, get_groups_version,
287+
fun () -> {0, 0} end),
288+
PidMap#{?FUNCTION_NAME => mocked}.
289+
290+
ns_secrets(PidMap) ->
291+
meck:expect(?FUNCTION_NAME, get_pkey_pass,
292+
fun (_) -> ?HIDE(undefined) end),
293+
PidMap#{?FUNCTION_NAME => mocked}.
294+
295+
dist_manager(PidMap) ->
296+
meck:expect(?FUNCTION_NAME, get_rename_txn_pid, fun () -> undefined end),
253297
PidMap#{?FUNCTION_NAME => mocked}.

0 commit comments

Comments
 (0)