1: -module(mam_proper_SUITE).
    2: -compile([export_all, nowarn_export_all]).
    3: -include_lib("proper/include/proper.hrl").
    4: -include_lib("eunit/include/eunit.hrl").
    5: 
    6: -import(distributed_helper, [mim/0,
    7:                              require_rpc_nodes/1,
    8:                              rpc/4]).
    9: -import(domain_helper, [host_type/0]).
   10: 
   11: %% Common Test init/teardown functions
   12: suite() ->
   13:     require_rpc_nodes([mim]) ++ escalus:suite().
   14: 
   15: all() ->
   16:     [{group, async_writer}].
   17: 
   18: groups() ->
   19:     [{async_writer, [], [async_writer_store]}].
   20: 
   21: init_per_suite(C) ->
   22:     application:ensure_all_started(jid),
   23:     C1 = dynamic_modules:save_modules(host_type(), C),
   24:     escalus:init_per_suite(C1).
   25: 
   26: end_per_suite(C) ->
   27:     dynamic_modules:restore_modules(C),
   28:     escalus:end_per_suite(C).
   29: 
   30: init_per_group(G, _C) ->
   31:     case mongoose_helper:is_rdbms_enabled(domain_helper:host_type()) of
   32:         true -> dynamic_modules:ensure_modules(host_type(), required_modules(G));
   33:         false -> {skip, "rdbms not enabled"}
   34:     end.
   35: 
   36: required_modules(_G) ->
   37:     [{mod_mam, mam_helper:config_opts(#{pm => #{}})}].
   38: 
   39: end_per_group(_G, C) ->
   40:     C.
   41: 
   42: init_per_testcase(Name, C) ->
   43:     escalus:init_per_testcase(Name, C).
   44: 
   45: end_per_testcase(Name, C) ->
   46:     escalus:end_per_testcase(Name, C).
   47: 
   48: %% Common Test Cases
   49: async_writer_store(C) ->
   50:     run_prop(async_writer_store, async_writer_store_prop(C)).
   51: 
   52: %% Proper Test Cases
   53: async_writer_store_prop(_C) ->
   54:     HostType = domain_helper:host_type(),
   55:     ?FORALL(ParamsList, params_list(), async_writer_store_check(HostType, ParamsList)).
   56: 
   57: async_writer_store_check(HostType, ParamsList) ->
   58:     clean_db(HostType),
   59:     Name = prepare_insert(length(ParamsList)),
   60:     Rows = [prepare_message(HostType, Params) || Params <- ParamsList],
   61:     case execute(HostType, Name, lists:append(Rows)) of
   62:         {updated, _} -> true;
   63:         Other ->
   64:             ct:pal("ParamsList ~p~nOther ~p", [ParamsList, Other]),
   65:             false
   66:     end.
   67: 
   68: %% Proper Type Generators
   69: usernames() ->
   70:    [<<"alice">>, <<"bob">>, <<"bob.1">>, <<"bob44">>, <<"a">>, <<"xd">>,
   71:     <<"admin">>].
   72: 
   73: resources() ->
   74:     [<<"res1">>, <<"PICKLE">>, <<"1">>, <<"xdxd">>].
   75: 
   76: origin_id() ->
   77:     oneof([none, <<"orig_id">>,
   78:            proper_types:non_empty(strip_nulls(proper_unicode:utf8(5)))]).
   79: 
   80: strip_nulls(Gen) ->
   81:     ?LET(X, Gen, do_strip_nulls(X)).
   82: 
   83: do_strip_nulls(B) ->
   84:     %% PostgreSQL doesn't support storing NULL (\0x00) characters in text fields
   85:     List = unicode:characters_to_list(B),
   86:     List2 = [X || X <- List, X =/= 0],
   87:     unicode:characters_to_binary(List2).
   88: 
   89: username() ->
   90:     ?SUCHTHAT(U, proper_types:non_empty(oneof(usernames())),
   91:               jid:nodeprep(U) =/= error).
   92: 
   93: hostname() ->
   94:     proper_types:oneof([<<"localhost">>, <<"otherhost">>, <<"example.com">>]).
   95: 
   96: resource() ->
   97:     ?SUCHTHAT(R, proper_types:non_empty(oneof(resources())),
   98:               jid:resourceprep(R) =/= error).
   99: 
  100: jid() ->
  101:     ?SUCHTHAT(Jid, maybe_invalid_jid(), Jid =/= error).
  102: 
  103: maybe_invalid_jid() ->
  104:     ?LET({U, S, R},
  105:          {username(), hostname(), resource()},
  106:          make_jid(U, S, R)).
  107: 
  108: make_jid(U, S, R) when is_binary(U), is_binary(S), is_binary(R) ->
  109:     mongoose_helper:make_jid(U, S, R).
  110: 
  111: direction() ->
  112:     proper_types:oneof([incoming, outgoing]).
  113: 
  114: body() ->
  115:     strip_nulls(proper_unicode:utf8(1000)).
  116: 
  117: packet(To, Body) ->
  118:     escalus_stanza:chat_to(jid:to_binary(To), Body).
  119: 
  120: %% Generates mod_mam:archive_message_params()
  121: params() ->
  122:     ?LET({MessId, ArcId, LocalJid, RemoteJid,
  123:           OriginId, Dir, Body, SenderId, IsGroupChat},
  124:          {non_neg_integer(), non_neg_integer(), jid(), jid(),
  125:           origin_id(), direction(), body(), non_neg_integer(), boolean()},
  126:          #{message_id => MessId, archive_id => ArcId,
  127:            local_jid => LocalJid, remote_jid => RemoteJid,
  128:            source_jid => choose_source_jid(Dir, LocalJid, RemoteJid),
  129:            origin_id => OriginId, direction => Dir,
  130:            packet => packet(choose_dest_jid(Dir, LocalJid, RemoteJid), Body),
  131:            sender_id => SenderId, is_groupchat => IsGroupChat}).
  132: 
  133: choose_source_jid(incoming, _LocalJid, RemoteJid) -> RemoteJid;
  134: choose_source_jid(outgoing, LocalJid, _RemoteJid) -> LocalJid.
  135: 
  136: choose_dest_jid(outgoing, _LocalJid, RemoteJid) -> RemoteJid;
  137: choose_dest_jid(incoming, LocalJid, _RemoteJid) -> LocalJid.
  138: 
  139: params_list() ->
  140:     ?LET(ParamsList, proper_types:non_empty(proper_types:list(params())),
  141:          with_unique_message_id(ParamsList)).
  142: 
  143: with_unique_message_id(ParamsList) ->
  144:     Pairs = [{MessId, Params} || #{message_id := MessId} = Params <- ParamsList],
  145:     %% Removes duplicates
  146:     maps:values(maps:from_list(Pairs)).
  147: 
  148: %% Proper Helper Functions
  149: run_prop(PropName, Property) ->
  150:     Opts = [verbose, long_result, {numtests, 30}],
  151:     ?assertEqual(true, quickcheck(PropName, Property, Opts)).
  152: 
  153: quickcheck(PropName, Property, Opts) ->
  154:     proper:quickcheck(proper:conjunction([{PropName, Property}]), Opts).
  155: 
  156: %% Helper Functions
  157: prepare_insert(Len) ->
  158:     Name = multi_name(insert_mam_message_prop, Len),
  159:     rpc(mim(), mod_mam_rdbms_arch, prepare_insert, [Name, Len]),
  160:     Name.
  161: 
  162: multi_name(Name, Times) ->
  163:     list_to_atom(atom_to_list(Name) ++ integer_to_list(Times)).
  164: 
  165: prepare_message(HostType, Params) ->
  166:     rpc(mim(), mod_mam_rdbms_arch, prepare_message, [HostType, Params]).
  167: 
  168: execute(HostType, BatchName, Args) ->
  169:     rpc(mim(), mongoose_rdbms, execute, [HostType, BatchName, Args]).
  170: 
  171: clean_db(HostType) ->
  172:     Q = <<"DELETE FROM mam_message">>,
  173:     rpc(mim(), mongoose_rdbms, sql_query, [HostType, Q]).