1: -module(keystore_SUITE).
    2: -include_lib("eunit/include/eunit.hrl").
    3: -compile([export_all, nowarn_export_all]).
    4: 
    5: -define(ae(Expected, Actual), ?assertEqual(Expected, Actual)).
    6: 
    7: all() ->
    8:     [
    9:      module_startup_no_opts,
   10:      module_startup_read_key_from_file,
   11:      module_startup_create_ram_key,
   12:      module_startup_create_ram_key_of_given_size,
   13:      module_startup_for_multiple_domains,
   14:      module_startup_non_unique_key_ids,
   15:      multiple_domains_one_stopped
   16:     ].
   17: 
   18: init_per_suite(C) ->
   19:     {ok, _} = application:ensure_all_started(jid),
   20:     ok = mnesia:create_schema([node()]),
   21:     ok = mnesia:start(),
   22:     C.
   23: 
   24: end_per_suite(C) ->
   25:     mnesia:stop(),
   26:     mnesia:delete_schema([node()]),
   27:     C.
   28: 
   29: init_per_testcase(_, Config) ->
   30:     mock_mongoose_metrics(),
   31:     async_helper:start(Config, gen_hook, start_link, []).
   32: 
   33: end_per_testcase(module_startup_non_unique_key_ids, C) ->
   34:     clean_after_testcase(C);
   35: end_per_testcase(module_startup_for_multiple_domains, C) ->
   36:     mod_keystore:stop(<<"first.com">>),
   37:     mod_keystore:stop(<<"second.com">>),
   38:     clean_after_testcase(C);
   39: end_per_testcase(multiple_domains_one_stopped, C) ->
   40:     mod_keystore:stop(<<"second.com">>),
   41:     clean_after_testcase(C);
   42: end_per_testcase(_CaseName, C) ->
   43:     mod_keystore:stop(<<"localhost">>),
   44:     clean_after_testcase(C).
   45: 
   46: clean_after_testcase(C) ->
   47:     meck:unload(mongoose_metrics),
   48:     async_helper:stop_all(C),
   49:     mnesia:delete_table(key),
   50:     C.
   51: 
   52: %%
   53: %% Tests
   54: %%
   55: 
   56: module_startup_no_opts(_) ->
   57:     ok = mod_keystore:start(<<"localhost">>, []).
   58: 
   59: module_startup_read_key_from_file(_) ->
   60:     %% given
   61:     RawKey = <<"qwe123">>,
   62:     {ok, KeyFile} = key_at("/tmp/key-from-file", RawKey),
   63:     %% when
   64:     ok = mod_keystore:start(<<"localhost">>, key_from_file(KeyFile)),
   65:     %% then
   66:     ?ae([{{key_from_file, <<"localhost">>}, RawKey}],
   67:         get_key(<<"localhost">>, key_from_file)).
   68: 
   69: module_startup_create_ram_key(Config) ->
   70:     module_startup_create_ram_key(Config, ram_key()),
   71:     %% then we can access the key
   72:     [{{ram_key, <<"localhost">>}, Key}] = get_key(<<"localhost">>, ram_key),
   73:     true = is_binary(Key).
   74: 
   75: module_startup_create_ram_key_of_given_size(Config) ->
   76:     KeySize = 4,
   77:     module_startup_create_ram_key(Config, sized_ram_key(KeySize)),
   78:     %% then
   79:     [{{ram_key, <<"localhost">>}, Key}] = get_key(<<"localhost">>, ram_key),
   80:     true = is_binary(Key),
   81:     KeySize = byte_size(Key).
   82: 
   83: module_startup_create_ram_key(_, ModKeystoreOpts) ->
   84:     %% given no key
   85:     [] = get_key(<<"localhost">>, ram_key),
   86:     %% when keystore starts with config to generate a memory-only key
   87:     ok = mod_keystore:start(<<"localhost">>, ModKeystoreOpts).
   88: 
   89: module_startup_for_multiple_domains(_Config) ->
   90:     %% given
   91:     [] = get_key(<<"first.com">>, key_from_file),
   92:     [] = get_key(<<"second.com">>, key_from_file),
   93:     FirstKey = <<"random-first.com-key-content">>,
   94:     SecondKey = <<"random-second.com-key-content">>,
   95:     {ok, FirstKeyFile} = key_at("/tmp/first.com", FirstKey),
   96:     {ok, SecondKeyFile} = key_at("/tmp/second.com", SecondKey),
   97:     %% when
   98:     ok = mod_keystore:start(<<"first.com">>, key_from_file(FirstKeyFile)),
   99:     ok = mod_keystore:start(<<"second.com">>, key_from_file(SecondKeyFile)),
  100:     %% then
  101:     ?ae([{{key_from_file, <<"first.com">>}, FirstKey}],
  102:         get_key(<<"first.com">>, key_from_file)),
  103:     ?ae([{{key_from_file, <<"second.com">>}, SecondKey}],
  104:         get_key(<<"second.com">>, key_from_file)).
  105: 
  106: module_startup_non_unique_key_ids(_) ->
  107:     %% given
  108:     NonUniqueKeyIDsOpts = [{keys, [{some_key, ram},
  109:                                    {some_key, {file, "some_key.dat"}}]}],
  110:     %% when
  111:     try
  112:         mod_keystore:start(<<"localhost">>, NonUniqueKeyIDsOpts)
  113:     %% then
  114:     catch
  115:         error:non_unique_key_ids -> ok
  116:     end.
  117: 
  118: multiple_domains_one_stopped(_Config) ->
  119:     % given
  120:     [] = get_key(<<"first.com">>, key_from_file),
  121:     [] = get_key(<<"second.com">>, key_from_file),
  122:     FirstKey = <<"random-first.com-key-content">>,
  123:     SecondKey = <<"random-second.com-key-content">>,
  124:     {ok, FirstKeyFile} = key_at("/tmp/first.com", FirstKey),
  125:     {ok, SecondKeyFile} = key_at("/tmp/second.com", SecondKey),
  126:     % when
  127:     ok = mod_keystore:start(<<"first.com">>, key_from_file(FirstKeyFile)),
  128:     ok = mod_keystore:start(<<"second.com">>, key_from_file(SecondKeyFile)),
  129:     ok = mod_keystore:stop(<<"first.com">>),
  130:     % then
  131:     ?ae([{{key_from_file, <<"second.com">>}, SecondKey}],
  132:         get_key(<<"second.com">>, key_from_file)).
  133: 
  134: %%
  135: %% Helpers
  136: %%
  137: 
  138: start_async(M, F, A) ->
  139:     Self = self(),
  140:     P = spawn(fun () ->
  141:                       erlang:apply(M, F, A),
  142:                       Self ! started,
  143:                       helper_loop()
  144:               end),
  145:     receive
  146:         started ->
  147:             %ct:pal("started", []),
  148:             {ok, P}
  149:         after timer:seconds(1) ->
  150:             ct:fail("async start timeout")
  151:     end.
  152: 
  153: helper_loop() ->
  154:     receive
  155:         stop -> exit(normal);
  156:         _    -> helper_loop()
  157:     end.
  158: 
  159: key_at(Path, Data) ->
  160:     ok = file:write_file(Path, Data),
  161:     {ok, Path}.
  162: 
  163: key_from_file(KeyFile) ->
  164:     [{keys, [{key_from_file, {file, KeyFile}}]}].
  165: 
  166: ram_key() ->
  167:     [{keys, [{ram_key, ram}]}].
  168: 
  169: sized_ram_key(Size) ->
  170:     [{keys, [{ram_key, ram}]},
  171:      {ram_key_size, Size}].
  172: 
  173: mock_mongoose_metrics() ->
  174:     meck:new(mongoose_metrics, []),
  175:     meck:expect(mongoose_metrics, create_generic_hook_metric, fun (_, _) -> ok end),
  176:     meck:expect(mongoose_metrics, increment_generic_hook_metric, fun (_, _) -> ok end),
  177:     ok.
  178: 
  179: %% Use a function like this in your module which is a client of mod_keystore.
  180: -spec get_key(HostType, KeyName) -> Result when
  181:       HostType :: mongooseim:host_type(),
  182:       KeyName :: mod_keystore:key_name(),
  183:       Result :: mod_keystore:key_list().
  184: get_key(HostType, KeyName) ->
  185:     mongoose_hooks:get_key(HostType, KeyName).
  186: 
  187: %%{mod_keystore, [{keys, [{asdqwe_access_secret, ram},
  188: %%                        {asdqwe_access_psk,    {file, "priv/asdqwe_access_psk"}},
  189: %%                        {asdqwe_provision_psk, {file, "priv/asdqwe_access_psk"}}]}]},