1: -module(mongoose_config_SUITE).
    2: -compile([export_all, nowarn_export_all]).
    3: 
    4: -include_lib("eunit/include/eunit.hrl").
    5: 
    6: -import(ejabberd_helper, [start_ejabberd/1,
    7:                           start_ejabberd_with_config/2,
    8:                           stop_ejabberd/0,
    9:                           use_config_file/2,
   10:                           copy/2,
   11:                           data/2]).
   12: 
   13: all() ->
   14:     [{group, opts},
   15:      {group, cluster}].
   16: 
   17: groups() ->
   18:     [
   19:      {opts, [parallel], [get_opt,
   20:                          lookup_opt,
   21:                          get_path,
   22:                          lookup_path,
   23:                          set_short_path,
   24:                          set_long_path,
   25:                          unset_path,
   26:                          load_from_file]},
   27:      {cluster, [], [cluster_load_from_file]}
   28:     ].
   29: 
   30: init_per_suite(Config) ->
   31:     {ok, _} = application:ensure_all_started(jid),
   32:     Config.
   33: 
   34: end_per_suite(_Config) ->
   35:     [persistent_term:erase(Key) || {Key = {mongoose_config, _}, _Value} <- persistent_term:get()],
   36:     persistent_term:erase(mongoose_config_state),
   37:     mnesia:stop(),
   38:     mnesia:delete_schema([node()]),
   39:     ok.
   40: 
   41: init_per_testcase(_TestCase, Config) ->
   42:     Config.
   43: 
   44: end_per_testcase(_TestCase, _Config) ->
   45:     ok.
   46: 
   47: init_per_group(cluster, Config) ->
   48:     start_slave_node(Config);
   49: init_per_group(_GroupName, Config) ->
   50:     Config.
   51: 
   52: end_per_group(cluster, Config) ->
   53:     stop_slave_node(Config),
   54:     ok;
   55: end_per_group(_GroupName, _Config) ->
   56:     ok.
   57: 
   58: %%
   59: %% Tests
   60: %%
   61: 
   62: get_opt(_Config) ->
   63:     ?assertError(badarg, mongoose_config:get_opt(get_me)),
   64:     ?assertEqual(default_value, mongoose_config:get_opt(get_me, default_value)),
   65:     mongoose_config:set_opt(get_me, you_got_me),
   66:     ?assertEqual(you_got_me, mongoose_config:get_opt(get_me)),
   67:     mongoose_config:set_opt(get_me, you_got_me_again),
   68:     ?assertEqual(you_got_me_again, mongoose_config:get_opt(get_me)),
   69:     ?assertEqual(you_got_me_again, mongoose_config:get_opt(get_me, default_value)),
   70:     mongoose_config:unset_opt(get_me),
   71:     ?assertError(badarg, mongoose_config:get_opt(get_me)),
   72:     ?assertEqual(default_value, mongoose_config:get_opt(get_me, default_value)).
   73: 
   74: lookup_opt(_Config) ->
   75:     ?assertEqual({error, not_found}, mongoose_config:lookup_opt(look_me_up)),
   76:     mongoose_config:set_opt(look_me_up, here_i_am),
   77:     ?assertEqual({ok, here_i_am}, mongoose_config:lookup_opt(look_me_up)),
   78:     mongoose_config:unset_opt(look_me_up),
   79:     ?assertEqual({error, not_found}, mongoose_config:lookup_opt(look_me_up)).
   80: 
   81: get_path(_Config) ->
   82:     ?assertError(badarg, mongoose_config:get_opt([root])),
   83:     ?assertError(badarg, mongoose_config:get_opt([root, branch])),
   84:     mongoose_config:set_opt(root, #{branch => leaf}),
   85:     ?assertEqual(#{branch => leaf}, mongoose_config:get_opt([root])),
   86:     ?assertEqual(leaf, mongoose_config:get_opt([root, branch])),
   87:     ?assertError({badmap, leaf}, mongoose_config:get_opt([root, branch, leaf])),
   88:     mongoose_config:unset_opt(root),
   89:     ?assertError(badarg, mongoose_config:get_opt([root])).
   90: 
   91: lookup_path(_Config) ->
   92:     ?assertEqual({error, not_found}, mongoose_config:lookup_opt([basement])),
   93:     ?assertEqual({error, not_found}, mongoose_config:lookup_opt([basement, floor])),
   94:     mongoose_config:set_opt(basement, #{floor => roof}),
   95:     ?assertEqual({ok, #{floor => roof}}, mongoose_config:lookup_opt([basement])),
   96:     ?assertEqual({ok, roof}, mongoose_config:lookup_opt([basement, floor])),
   97:     ?assertError({badmap, roof}, mongoose_config:lookup_opt([basement, floor, roof])),
   98:     mongoose_config:unset_opt(basement),
   99:     ?assertEqual({error, not_found}, mongoose_config:lookup_opt([basement])).
  100: 
  101: set_short_path(_Config) ->
  102:     mongoose_config:set_opt([a], 1),
  103:     ?assertEqual(1, mongoose_config:get_opt(a)),
  104:     mongoose_config:set_opt([a], 2),
  105:     ?assertEqual(2, mongoose_config:get_opt(a)),
  106:     ?assertError({badmap, 2}, mongoose_config:set_opt([a, b], c)),
  107:     ?assertEqual(2, mongoose_config:get_opt(a)).
  108: 
  109: set_long_path(_Config) ->
  110:     ?assertError(badarg, mongoose_config:set_opt([one, two, three], 4)),
  111:     mongoose_config:set_opt([one], #{}),
  112:     ?assertError({badkey, _}, mongoose_config:set_opt([one, two, three], 4)),
  113:     mongoose_config:set_opt([one, two], #{}),
  114:     mongoose_config:set_opt([one, two, three], 4),
  115:     ?assertEqual(#{two => #{three => 4}}, mongoose_config:get_opt(one)),
  116:     mongoose_config:set_opt([one, two], 3),
  117:     ?assertEqual(#{two => 3}, mongoose_config:get_opt(one)).
  118: 
  119: unset_path(_Config) ->
  120:     mongoose_config:set_opt(foo, #{bar => #{baz => boom}}),
  121:     ?assertEqual(#{bar => #{baz => boom}}, mongoose_config:get_opt(foo)),
  122:     ?assertError({badmap, boom}, mongoose_config:unset_opt([foo, bar, baz, boom])),
  123:     mongoose_config:unset_opt([foo, bar, baz]),
  124:     ?assertEqual(#{bar => #{}}, mongoose_config:get_opt(foo)), % empty map is not removed
  125:     mongoose_config:unset_opt([foo, bar, baz]), % no error for a non-existing key
  126:     ?assertEqual(#{bar => #{}}, mongoose_config:get_opt(foo)),
  127:     mongoose_config:unset_opt([foo]),
  128:     ?assertError(badarg, mongoose_config:get_opt(foo)),
  129:     ?assertError(badarg, mongoose_config:unset_opt([foo, bar])),
  130:     mongoose_config:unset_opt([foo]). % no error for a non-existing key
  131: 
  132: load_from_file(Config) ->
  133:     use_config_file(Config, "mongooseim.minimal.toml"),
  134:     ok = mongoose_config:start(),
  135:     State = mongoose_config:config_state(),
  136:     check_loaded_config(State),
  137: 
  138:     ok = mongoose_config:stop(),
  139:     check_removed_config(),
  140: 
  141:     %% Try to stop it again
  142:     {error, not_started} = mongoose_config:stop().
  143: 
  144: cluster_load_from_file(Config) ->
  145:     SlaveNode = slave_node(Config),
  146:     copy(data(Config, "mongooseim.minimal.toml"), data(Config, "mongooseim.toml")),
  147: 
  148:     %% Start clustered MongooseIM and check the loaded config
  149:     {ok, _} = start_ejabberd_with_config(Config, "mongooseim.toml"),
  150:     {ok, _} = start_remote_ejabberd_with_config(SlaveNode, Config, "mongooseim.toml"),
  151:     maybe_join_cluster(SlaveNode),
  152:     [State, State] = mongoose_config:config_states(),
  153:     check_loaded_config(State),
  154: 
  155:     ok = stop_ejabberd(),
  156:     stop_remote_ejabberd(SlaveNode),
  157:     check_removed_config().
  158: 
  159: %%
  160: %% Helpers
  161: %%
  162: 
  163: check_loaded_config(State) ->
  164:     Opts = lists:sort(mongoose_config_parser:get_opts(State)),
  165:     ExpectedOpts = lists:sort(minimal_config_opts()),
  166:     ?assertEqual(ExpectedOpts, Opts),
  167:     [?assertEqual(Val, mongoose_config:get_opt(Key)) || {Key, Val} <- ExpectedOpts].
  168: 
  169: check_removed_config() ->
  170:     Opts = minimal_config_opts(),
  171:     ?assertError(badarg, mongoose_config:config_state()),
  172:     [?assertError(badarg, mongoose_config:get_opt(Key)) || {Key, _} <- Opts].
  173: 
  174: minimal_config_opts() ->
  175:     [{all_metrics_are_global, false},
  176:      {default_server_domain, <<"localhost">>},
  177:      {hide_service_name, false},
  178:      {host_types, []},
  179:      {hosts, [<<"localhost">>]},
  180:      {language, <<"en">>},
  181:      {listen, []},
  182:      {loglevel, warning},
  183:      {mongooseimctl_access_commands, []},
  184:      {rdbms_server_type, generic},
  185:      {registration_timeout, 600},
  186:      {routing_modules, mongoose_router:default_routing_modules()},
  187:      {sm_backend, {mnesia, []}},
  188:      {{auth, <<"localhost">>}, config_parser_helper:default_auth()},
  189:      {{modules, <<"localhost">>}, #{}},
  190:      {{replaced_wait_timeout, <<"localhost">>}, 2000}].
  191: 
  192: start_slave_node(Config) ->
  193:     SlaveNode = do_start_slave_node(),
  194:     [{slave_node, SlaveNode}|Config].
  195: 
  196: do_start_slave_node() ->
  197:     Opts = [{monitor_master, true},
  198:             {boot_timeout, 15}, %% in seconds
  199:             {init_timeout, 10}, %% in seconds
  200:             {startup_timeout, 10}], %% in seconds
  201:     {ok, SlaveNode} = ct_slave:start(slave_name(), Opts),
  202:     {ok, CWD} = file:get_cwd(),
  203:     ok = rpc:call(SlaveNode, file, set_cwd, [CWD]),
  204:     %% Tell the remote node where to find the SUITE code
  205:     %% Be aware, that P1 likes to put there stuff into
  206:     %% /usr/lib/erlang/lib/
  207:     %% So add_paths is NOT enough here
  208:     ok = rpc:call(SlaveNode, code, add_pathsa, [lists:reverse(code_paths())]),
  209:     check_that_p1_tls_is_correct(SlaveNode),
  210:     SlaveNode.
  211: 
  212: check_that_p1_tls_is_correct(SlaveNode) ->
  213:     ?assertEqual(fast_tls:module_info(md5),
  214:                  rpc:call(SlaveNode, fast_tls, module_info, [md5])).
  215: 
  216: stop_slave_node(Config) ->
  217:     ct_slave:stop(slave_node(Config)),
  218:     ok.
  219: 
  220: slave_node(Config) ->
  221:     get_required_config(slave_node, Config).
  222: 
  223: get_required_config(Key, Config) ->
  224:     case proplists:get_value(Key, Config) of
  225:         undefined ->
  226:             ct:fail({get_required_config_failed, Key});
  227:         Value ->
  228:             Value
  229:     end.
  230: 
  231: slave_name() ->
  232:     'mim_slave'.
  233: 
  234: start_remote_ejabberd_with_config(RemoteNode, C, ConfigFile) ->
  235:     rpc:call(RemoteNode, ejabberd_helper, start_ejabberd_with_config, [C, ConfigFile]).
  236: 
  237: stop_remote_ejabberd(SlaveNode) ->
  238:     rpc:call(SlaveNode, ejabberd_helper, stop_ejabberd, []).
  239: 
  240: code_paths() ->
  241:     [filename:absname(Path) || Path <- code:get_path()].
  242: 
  243: maybe_join_cluster(SlaveNode) ->
  244:     Result = rpc:call(SlaveNode, ejabberd_admin, join_cluster,
  245:                       [atom_to_list(node())]),
  246:     case Result of
  247:         {ok, _} ->
  248:             ok;
  249:         {already_joined, _} ->
  250:             ok
  251:     end.
  252: