1: -module(router_SUITE).
    2: -compile([export_all, nowarn_export_all]).
    3: 
    4: -include_lib("exml/include/exml.hrl").
    5: -include_lib("eunit/include/eunit.hrl").
    6: -include("mongoose.hrl").
    7: 
    8: %% ---------------------------------------------------------------
    9: %% Common Test callbacks
   10: %% ---------------------------------------------------------------
   11: 
   12: all() ->
   13:     [
   14:      {group, routing},
   15:      {group, schema}
   16:     ].
   17: 
   18: groups() ->
   19:     [
   20:      {routing, [], [
   21:                     basic_routing,
   22:                     do_not_reroute_errors
   23:                    ]},
   24:      {schema, [], [
   25:                    update_tables_hidden_components,
   26:                    update_tables_hidden_components_idempotent
   27:                   ]}
   28:     ].
   29: 
   30: init_per_suite(C) ->
   31:     {ok, _} = application:ensure_all_started(jid),
   32:     ok = mnesia:create_schema([node()]),
   33:     ok = mnesia:start(),
   34:     {ok, _} = application:ensure_all_started(exometer_core),
   35:     C.
   36: 
   37: end_per_suite(_C) ->
   38:     meck:unload(),
   39:     mnesia:stop(),
   40:     mnesia:delete_schema([node()]),
   41:     application:stop(exometer_core),
   42:     ok.
   43: 
   44: init_per_group(routing, Config) ->
   45:     mongoose_config:set_opt(routing_modules, [xmpp_router_a, xmpp_router_b, xmpp_router_c]),
   46:     gen_hook:start_link(),
   47:     ejabberd_router:start_link(),
   48:     Config;
   49: init_per_group(schema, Config) ->
   50:     remove_component_tables(),
   51:     Config.
   52: 
   53: end_per_group(routing, _Config) ->
   54:     mongoose_config:unset_opt(routing_modules);
   55: end_per_group(schema, _Config) ->
   56:     ok.
   57: 
   58: init_per_testcase(_CaseName, Config) ->
   59:     Config.
   60: 
   61: end_per_testcase(HiddenComponent, _Config)
   62:   when HiddenComponent == update_tables_hidden_components;
   63:        HiddenComponent == update_tables_hidden_components_idempotent ->
   64:     remove_component_tables();
   65: end_per_testcase(_CaseName, _Config) ->
   66:     ok.
   67: 
   68: %% ---------------------------------------------------------------
   69: %% Test cases
   70: %% ---------------------------------------------------------------
   71: 
   72: basic_routing(_C) ->
   73:     %% module 'a' drops message 1, routes message 2, passes on everything else
   74:     setup_routing_module(xmpp_router_a, 1, 2),
   75:     %% module 'b' drops message 3, routes message 4, passes on everything else
   76:     setup_routing_module(xmpp_router_b, 3, 4),
   77:     %% module 'c' routes everything
   78:     setup_routing_module(xmpp_router_c, none, all),
   79:     %% send messages from 1 to 5
   80:     lists:map(fun(I) -> route(msg(I)) end, [1,2,3,4,5]),
   81:     meck:validate(xmpp_router_a),
   82:     meck:unload(xmpp_router_a),
   83:     meck:validate(xmpp_router_b),
   84:     meck:unload(xmpp_router_b),
   85:     meck:validate(xmpp_router_c),
   86:     meck:unload(xmpp_router_c),
   87:     %% we know that 1 and 3 should be dropped, and 2, 4 and 5 handled by a, b and c respectively
   88:     verify([{a, 2}, {b, 4}, {c, 5}]),
   89:     ok.
   90: 
   91: %% This test makes sure that if we try to respond to an error message by routing error message
   92: %% we do not enter an infinite loop; it has been fixed in d3941e33453c95ca78561144182712cc4f1d6c72
   93: %% without the fix this tests gets stuck in a loop.
   94: do_not_reroute_errors(_) ->
   95:     From = <<"ja@localhost">>,
   96:     To = <<"ty@localhost">>,
   97:     Stanza = #xmlel{name = <<"iq">>,
   98:         attrs = [{<<"from">>, From}, {<<"to">>, To}, {<<"type">>, <<"get">>} ]
   99:     },
  100:     Acc = mongoose_acc:new(#{ location => ?LOCATION,
  101:                               lserver => <<"localhost">>,
  102:                               host_type => <<"localhost">>,
  103:                               element => Stanza }),
  104:     meck:new(xmpp_router_a, [non_strict]),
  105:     meck:expect(xmpp_router_a, filter,
  106:                 fun(From0, To0, Acc0, Packet0) -> {From0, To0, Acc0, Packet0} end),
  107:     meck:expect(xmpp_router_a, route, fun resend_as_error/4),
  108:     ejabberd_router:route(From, To, Acc, Stanza),
  109:     ok.
  110: 
  111: update_tables_hidden_components(_C) ->
  112:     %% Tables as of b076e4a62a8b21188245f13c42f9cfd93e06e6b7
  113:     create_component_tables([domain, handler, node]),
  114: 
  115:     ejabberd_router:update_tables(),
  116: 
  117:     %% Local table is removed and the distributed one has a new list of attributes
  118:     false = lists:member(external_component, mnesia:system_info(tables)),
  119:     [domain, handler, node, is_hidden] = mnesia:table_info(external_component_global, attributes).
  120: 
  121: update_tables_hidden_components_idempotent(_C) ->
  122:     AttrsWithHidden = [domain, handler, node, is_hidden],
  123:     create_component_tables(AttrsWithHidden),
  124: 
  125:     ejabberd_router:update_tables(),
  126: 
  127:     %% Local table is not removed and the attribute list of the distributed one is not changed
  128:     true = lists:member(external_component, mnesia:system_info(tables)),
  129:     AttrsWithHidden = mnesia:table_info(external_component_global, attributes).
  130: 
  131: %% ---------------------------------------------------------------
  132: %% Helpers
  133: %% ---------------------------------------------------------------
  134: 
  135: setup_routing_module(Name, PacketToDrop, PacketToRoute) ->
  136:     meck:new(Name, [non_strict]),
  137:     meck:expect(Name, filter,
  138:         fun(From, To, Acc, Packet) ->
  139:             case msg_to_id(Packet) of
  140:                 PacketToDrop -> drop;
  141:                 _ -> {From, To, Acc, Packet}
  142:             end
  143:         end),
  144:     meck:expect(Name, route,
  145:         make_routing_fun(Name, PacketToRoute)),
  146:     ok.
  147: 
  148: make_routing_fun(Name, all) ->
  149:     Self = self(),
  150:     Marker = list_to_atom([lists:last(atom_to_list(Name))]),
  151:     fun(_From, _To, Acc, Packet) ->
  152:         Self ! {Marker, Packet},
  153:         {done, Acc}
  154:     end;
  155: make_routing_fun(Name, PacketToRoute) ->
  156:     Self = self(),
  157:     Marker = list_to_atom([lists:last(atom_to_list(Name))]),
  158:     fun(From, To, Acc, Packet) ->
  159:         case msg_to_id(Packet) of
  160:             PacketToRoute ->
  161:                 Self ! {Marker, Packet},
  162:                 {done, Acc};
  163:             _ -> {From, To, Acc, Packet}
  164:         end
  165:     end.
  166: 
  167: msg(I) ->
  168:     IBin = integer_to_binary(I),
  169:     #xmlel{ name = <<"message">>,
  170:             children = [
  171:                         #xmlel{ name = <<"body">>,
  172:                                 children = [#xmlcdata{ content = IBin }] }
  173:                        ] }.
  174: 
  175: msg_to_id(Msg) ->
  176:     binary_to_integer(exml_query:path(Msg, [{element, <<"body">>}, cdata])).
  177: 
  178: route(I) ->
  179:     FromJID = jid:from_binary(<<"ala@localhost">>),
  180:     ToJID = jid:from_binary(<<"bob@localhost">>),
  181:     Acc = mongoose_acc:new(#{ location => ?LOCATION,
  182:                               lserver => <<"localhost">>,
  183:                               host_type => <<"localhost">>,
  184:                               element => I,
  185:                               from_jid => FromJID,
  186:                               to_jid => ToJID }),
  187:     #{} = ejabberd_router:route(FromJID, ToJID, Acc, I).
  188: 
  189: verify(L) ->
  190:     receive
  191:         {RouterID, XML} ->
  192:             X = msg_to_id(XML),
  193:             ct:pal("{RouterID, X, L}: ~p", [{RouterID, X, L}]),
  194:             Item = {RouterID, X},
  195:             ?assert(lists:member(Item, L)),
  196:             verify(lists:delete(Item, L))
  197:     after 1000 ->
  198:         ?assertEqual(L, []),
  199:         ct:pal("all messages routed correctly")
  200:     end.
  201: 
  202: create_component_tables(AttrList) ->
  203:     {atomic, ok} =
  204:     mnesia:create_table(external_component,
  205:                         [{attributes, AttrList},
  206:                          {local_content, true}]),
  207:     {atomic, ok} =
  208:     mnesia:create_table(external_component_global,
  209:                         [{attributes, AttrList},
  210:                          {type, bag},
  211:                          {record_name, external_component}]).
  212: 
  213: remove_component_tables() ->
  214:     mnesia:delete_table(external_component),
  215:     mnesia:delete_table(external_component_global).
  216: 
  217: resend_as_error(From0, To0, Acc0, Packet0) ->
  218:     {Acc1, Packet1} = jlib:make_error_reply(Acc0, Packet0, #xmlel{}),
  219:     Acc2 = ejabberd_router:route(To0, From0, Acc1, Packet1),
  220:     {done, Acc2}.