1: -module(mongoose_subdomain_core_SUITE).
    2: 
    3: -compile([export_all, nowarn_export_all]).
    4: 
    5: -include_lib("eunit/include/eunit.hrl").
    6: 
    7: -define(STATIC_HOST_TYPE, <<"static type">>).
    8: -define(STATIC_DOMAIN, <<"example.com">>).
    9: -define(DYNAMIC_HOST_TYPE1, <<"dynamic type #1">>).
   10: -define(DYNAMIC_HOST_TYPE2, <<"dynamic type #2">>).
   11: -define(DYNAMIC_DOMAINS, [<<"localhost">>, <<"local.host">>]).
   12: -define(STATIC_PAIRS, [{?STATIC_DOMAIN, ?STATIC_HOST_TYPE}]).
   13: -define(ALLOWED_HOST_TYPES, [?DYNAMIC_HOST_TYPE1, ?DYNAMIC_HOST_TYPE2]).
   14: 
   15: -define(assertEqualLists(L1, L2), ?assertEqual(lists:sort(L1), lists:sort(L2))).
   16: 
   17: all() ->
   18:     [can_register_and_unregister_subdomain_for_static_host_type,
   19:      can_register_and_unregister_subdomain_for_dynamic_host_type_with_domains,
   20:      can_register_and_unregister_subdomain_for_dynamic_host_type_without_domains,
   21:      can_register_and_unregister_fqdn_for_static_host_type,
   22:      can_register_and_unregister_fqdn_for_dynamic_host_type_with_domains,
   23:      can_register_and_unregister_fqdn_for_dynamic_host_type_without_domains,
   24:      can_add_and_remove_domain,
   25:      can_get_host_type_and_subdomain_details,
   26:      handles_domain_removal_during_subdomain_registration,
   27:      prevents_double_subdomain_registration,
   28:      prevents_prefix_subdomain_overriding_by_prefix_subdomain,
   29:      prevents_fqdn_subdomain_overriding_by_prefix_subdomain,
   30:      prevents_prefix_subdomain_overriding_by_fqdn_subdomain,
   31:      prevents_fqdn_subdomain_overriding_by_fqdn_subdomain,
   32:      detects_domain_conflict_with_prefix_subdomain,
   33:      detects_domain_conflict_with_fqdn_subdomain].
   34: 
   35: init_per_testcase(TestCase, Config) ->
   36:     %% mongoose_domain_core preconditions:
   37:     %%   - one "static" host type with only one configured domain name
   38:     %%   - one "dynamic" host type without any configured domain names
   39:     %%   - one "dynamic" host type with two configured domain names
   40:     %% initial mongoose_subdomain_core conditions:
   41:     %%   - no subdomains configured for any host type
   42:     mongooseim_helper:start_link_loaded_hooks(),
   43:     mongoose_domain_sup:start_link(?STATIC_PAIRS, ?ALLOWED_HOST_TYPES),
   44:     [mongoose_domain_core:insert(Domain, ?DYNAMIC_HOST_TYPE2, dummy_source)
   45:      || Domain <- ?DYNAMIC_DOMAINS],
   46:     setup_meck(TestCase),
   47:     Config.
   48: 
   49: end_per_testcase(_, Config) ->
   50:     meck:unload(),
   51:     Config.
   52: 
   53: %%-------------------------------------------------------------------
   54: %% normal test cases
   55: %%-------------------------------------------------------------------
   56: can_register_and_unregister_subdomain_for_static_host_type(_Config) ->
   57:     Handler = mongoose_packet_handler:new(?MODULE),
   58:     Pattern = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"),
   59:     Subdomain = mongoose_subdomain_utils:get_fqdn(Pattern, ?STATIC_DOMAIN),
   60:     %% register one "prefix" subdomain for static host type.
   61:     %% check that ETS table contains expected subdomain and nothing else.
   62:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?STATIC_HOST_TYPE,
   63:                                                                 Pattern, Handler)),
   64:     ?assertEqual([Subdomain], get_all_subdomains()),
   65:     ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?STATIC_HOST_TYPE,
   66:                                                                   Pattern)),
   67:     ?assertEqual([], get_all_subdomains()),
   68:     ?assertEqual([Subdomain], get_list_of_disabled_subdomains()),
   69:     no_collisions().
   70: 
   71: can_register_and_unregister_subdomain_for_dynamic_host_type_with_domains(_Config) ->
   72:     Handler = mongoose_packet_handler:new(?MODULE),
   73:     Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"),
   74:     Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain2.@HOST@"),
   75:     Subdomains1 = [mongoose_subdomain_utils:get_fqdn(Pattern1, Domain)
   76:                    || Domain <- ?DYNAMIC_DOMAINS],
   77:     Subdomains2 = [mongoose_subdomain_utils:get_fqdn(Pattern2, Domain)
   78:                    || Domain <- ?DYNAMIC_DOMAINS],
   79:     %% register one "prefix" subdomain for dynamic host type with 2 domains.
   80:     %% check that ETS table contains all the expected subdomains and nothing else.
   81:     %% make a snapshot of subdomains ETS table and check its size.
   82:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2,
   83:                                                                 Pattern1, Handler)),
   84:     ?assertEqualLists(Subdomains1, get_all_subdomains()),
   85:     %% register one more "prefix" subdomain for dynamic host type with 2 domains.
   86:     %% check that ETS table contains all the expected subdomains and nothing else.
   87:     %% check ETS table size.
   88:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2,
   89:                                                                 Pattern2, Handler)),
   90:     ?assertEqualLists(Subdomains1 ++ Subdomains2, get_all_subdomains()),
   91:     %% check mongoose_subdomain_core:get_all_subdomains_for_domain/1 interface.
   92:     [DynamicDomain | _] = ?DYNAMIC_DOMAINS,
   93:     HostTypeExtra = #{host_type => ?DYNAMIC_HOST_TYPE2},
   94:     HandlerWithHostType = mongoose_packet_handler:add_extra(Handler, HostTypeExtra),
   95:     ?assertEqualLists(
   96:         [#{host_type => ?DYNAMIC_HOST_TYPE2, subdomain_pattern => Pattern1,
   97:            parent_domain => DynamicDomain, packet_handler => HandlerWithHostType,
   98:            subdomain => mongoose_subdomain_utils:get_fqdn(Pattern1, DynamicDomain)},
   99:          #{host_type => ?DYNAMIC_HOST_TYPE2, subdomain_pattern => Pattern2,
  100:            parent_domain => DynamicDomain, packet_handler => HandlerWithHostType,
  101:            subdomain => mongoose_subdomain_utils:get_fqdn(Pattern2, DynamicDomain)}],
  102:         mongoose_subdomain_core:get_all_subdomains_for_domain(DynamicDomain)),
  103:     %% unregister (previously registered) subdomains one by one.
  104:     %% check that ETS table rolls back to the previously made snapshot.
  105:     ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2,
  106:                                                                   Pattern2)),
  107:     ?assertEqualLists(Subdomains1, get_all_subdomains()),
  108:     ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2,
  109:                                                                   Pattern1)),
  110:     ?assertEqual([], get_all_subdomains()),
  111:     ?assertEqualLists(Subdomains1 ++ Subdomains2, get_list_of_disabled_subdomains()),
  112:     no_collisions().
  113: 
  114: can_register_and_unregister_subdomain_for_dynamic_host_type_without_domains(_Config) ->
  115:     Handler = mongoose_packet_handler:new(?MODULE),
  116:     Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"),
  117:     Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain2.@HOST@"),
  118:     %% register two "prefix" subdomains for dynamic host type with 0 domains.
  119:     %% check that ETS table doesn't contain any subdomains.
  120:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  121:                                                                 Pattern1, Handler)),
  122:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  123:                                                                 Pattern2, Handler)),
  124:     ?assertEqual([], get_all_subdomains()),
  125:     %% unregister (previously registered) subdomains one by one.
  126:     ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE1,
  127:                                                                   Pattern1)),
  128:     ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE1,
  129:                                                                   Pattern2)),
  130:     ?assertEqual([], get_all_subdomains()),
  131:     ?assertEqual([], get_list_of_disabled_subdomains()),
  132:     no_collisions().
  133: 
  134: can_register_and_unregister_fqdn_for_static_host_type(_Config) ->
  135:     Pattern = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"),
  136:     Handler = mongoose_packet_handler:new(?MODULE),
  137:     %% register one FQDN subdomain for static host type.
  138:     %% check that ETS table contains the only expected subdomain.
  139:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?STATIC_HOST_TYPE,
  140:                                                                 Pattern, Handler)),
  141:     ?assertEqual([<<"some.fqdn">>], get_all_subdomains()),
  142:     %% unregister subdomain.
  143:     ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?STATIC_HOST_TYPE,
  144:                                                                   Pattern)),
  145:     ?assertEqual([], get_all_subdomains()),
  146:     ?assertEqual([<<"some.fqdn">>], get_list_of_disabled_subdomains()),
  147:     no_collisions().
  148: 
  149: can_register_and_unregister_fqdn_for_dynamic_host_type_without_domains(_Config) ->
  150:     Pattern = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"),
  151:     Handler = mongoose_packet_handler:new(?MODULE),
  152:     %% register one FQDN subdomain for dynamic host type with 0 domains.
  153:     %% check that ETS table contains the only expected subdomain.
  154:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  155:                                                                 Pattern, Handler)),
  156:     ?assertEqual([<<"some.fqdn">>], get_all_subdomains()),
  157:     %% unregister subdomain.
  158:     ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE1,
  159:                                                                   Pattern)),
  160:     ?assertEqual([], get_all_subdomains()),
  161:     ?assertEqual([<<"some.fqdn">>], get_list_of_disabled_subdomains()),
  162:     no_collisions().
  163: 
  164: can_register_and_unregister_fqdn_for_dynamic_host_type_with_domains(_Config) ->
  165:     Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"),
  166:     Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("another.fqdn"),
  167:     Handler = mongoose_packet_handler:new(?MODULE),
  168:     %% register one FQDN subdomain for dynamic host type with 2 domains.
  169:     %% check that ETS table contains all the expected subdomains and nothing else.
  170:     %% make a snapshot of subdomains ETS table.
  171:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2,
  172:                                                                 Pattern1, Handler)),
  173:     ?assertEqual([<<"some.fqdn">>], get_all_subdomains()),
  174:     %% register one more FQDN subdomain for dynamic host type with 2 domains.
  175:     %% check mongoose_subdomain_core:get_all_subdomains_for_domain/1 interface
  176:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2,
  177:                                                                 Pattern2, Handler)),
  178:     ?assertEqualLists([<<"some.fqdn">>, <<"another.fqdn">>], get_all_subdomains()),
  179:     HostTypeExtra = #{host_type => ?DYNAMIC_HOST_TYPE2},
  180:     HandlerWithHostType = mongoose_packet_handler:add_extra(Handler, HostTypeExtra),
  181:     ?assertEqualLists(
  182:         [#{host_type => ?DYNAMIC_HOST_TYPE2, parent_domain => no_parent_domain,
  183:            subdomain_pattern => Pattern1, packet_handler => HandlerWithHostType,
  184:            subdomain => <<"some.fqdn">>},
  185:          #{host_type => ?DYNAMIC_HOST_TYPE2, parent_domain => no_parent_domain,
  186:            subdomain_pattern => Pattern2, packet_handler => HandlerWithHostType,
  187:            subdomain => <<"another.fqdn">>}],
  188:         mongoose_subdomain_core:get_all_subdomains_for_domain(no_parent_domain)),
  189:     %% unregister (previously registered) subdomains one by one.
  190:     %% check that ETS table rolls back to the previously made snapshot.
  191:     ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2,
  192:                                                                   Pattern2)),
  193:     ?assertEqual([<<"some.fqdn">>], get_all_subdomains()),
  194:     ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2,
  195:                                                                   Pattern1)),
  196:     ?assertEqual([], get_all_subdomains()),
  197:     ?assertEqualLists([<<"some.fqdn">>, <<"another.fqdn">>],
  198:                       get_list_of_disabled_subdomains()),
  199:     no_collisions().
  200: 
  201: can_add_and_remove_domain(_Config) ->
  202:     Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"),
  203:     Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain2.@HOST@"),
  204:     Pattern3 = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"),
  205:     Handler = mongoose_packet_handler:new(?MODULE),
  206:     Subdomains1 = [mongoose_subdomain_utils:get_fqdn(Pattern1, Domain)
  207:                    || Domain <- ?DYNAMIC_DOMAINS],
  208:     Subdomains2 = [mongoose_subdomain_utils:get_fqdn(Pattern2, Domain)
  209:                    || Domain <- ?DYNAMIC_DOMAINS],
  210:     ?assertEqual([], get_all_subdomains()),
  211:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2,
  212:                                                                 Pattern1, Handler)),
  213:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2,
  214:                                                                 Pattern2, Handler)),
  215:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2,
  216:                                                                 Pattern3, Handler)),
  217:     ?assertEqualLists([<<"some.fqdn">> | Subdomains1 ++ Subdomains2],
  218:                       get_all_subdomains()),
  219:     [DynamicDomain | _] = ?DYNAMIC_DOMAINS,
  220:     mongoose_domain_core:delete(DynamicDomain),
  221:     ?assertEqualLists([<<"some.fqdn">> | tl(Subdomains1) ++ tl(Subdomains2)],
  222:                       get_all_subdomains()),
  223:     ?assertEqualLists([hd(Subdomains1), hd(Subdomains2)],
  224:                       get_list_of_disabled_subdomains()),
  225:     mongoose_domain_core:insert(DynamicDomain, ?DYNAMIC_HOST_TYPE2, dummy_source),
  226:     ?assertEqualLists([<<"some.fqdn">> | Subdomains1 ++ Subdomains2],
  227:                       get_all_subdomains()),
  228:     no_collisions().
  229: 
  230: can_get_host_type_and_subdomain_details(_Config) ->
  231:     Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"),
  232:     Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"),
  233:     Handler = mongoose_packet_handler:new(?MODULE),
  234:     Subdomain1 = mongoose_subdomain_utils:get_fqdn(Pattern1, ?STATIC_DOMAIN),
  235:     Subdomain2 = mongoose_subdomain_utils:get_fqdn(Pattern1, hd(?DYNAMIC_DOMAINS)),
  236:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?STATIC_HOST_TYPE,
  237:                                                                 Pattern1, Handler)),
  238:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2,
  239:                                                                 Pattern1, Handler)),
  240:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  241:                                                                 Pattern2, Handler)),
  242:     mongoose_subdomain_core:sync(),
  243:     ?assertEqual({ok, ?STATIC_HOST_TYPE},
  244:                  mongoose_subdomain_core:get_host_type(Subdomain1)),
  245:     ?assertEqual({ok, ?DYNAMIC_HOST_TYPE1},
  246:                  mongoose_subdomain_core:get_host_type(<<"some.fqdn">>)),
  247:     ?assertEqual({ok, ?DYNAMIC_HOST_TYPE2},
  248:                  mongoose_subdomain_core:get_host_type(Subdomain2)),
  249:     ?assertEqual({error, not_found},
  250:                  mongoose_subdomain_core:get_host_type(<<"unknown.subdomain">>)),
  251:     HostTypeExtra1 = #{host_type => ?STATIC_HOST_TYPE},
  252:     HandlerWithHostType1 = mongoose_packet_handler:add_extra(Handler, HostTypeExtra1),
  253:     ?assertEqual({ok, #{host_type => ?STATIC_HOST_TYPE, subdomain_pattern => Pattern1,
  254:                         parent_domain => ?STATIC_DOMAIN, subdomain => Subdomain1,
  255:                         packet_handler => HandlerWithHostType1}},
  256:                  mongoose_subdomain_core:get_subdomain_info(Subdomain1)),
  257:     HostTypeExtra2 = #{host_type => ?DYNAMIC_HOST_TYPE1},
  258:     HandlerWithHostType2 = mongoose_packet_handler:add_extra(Handler, HostTypeExtra2),
  259:     ?assertEqual({ok, #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern2,
  260:                         parent_domain => no_parent_domain, subdomain => <<"some.fqdn">>,
  261:                         packet_handler => HandlerWithHostType2}},
  262:                  mongoose_subdomain_core:get_subdomain_info(<<"some.fqdn">>)),
  263:     HostTypeExtra3 = #{host_type => ?DYNAMIC_HOST_TYPE2},
  264:     HandlerWithHostType3 = mongoose_packet_handler:add_extra(Handler, HostTypeExtra3),
  265:     ?assertEqual({ok, #{host_type => ?DYNAMIC_HOST_TYPE2, subdomain_pattern => Pattern1,
  266:                         parent_domain => hd(?DYNAMIC_DOMAINS), subdomain => Subdomain2,
  267:                         packet_handler => HandlerWithHostType3}},
  268:                  mongoose_subdomain_core:get_subdomain_info(Subdomain2)),
  269:     ?assertEqual({error, not_found},
  270:                  mongoose_subdomain_core:get_subdomain_info(<<"unknown.subdomain">>)),
  271:     ok.
  272: 
  273: handles_domain_removal_during_subdomain_registration(_Config) ->
  274:     %% NumOfDomains is just some big non-round number to ensure that more than 2 ets
  275:     %% selections are done during the call to mongoose_domain_core:for_each_domain/2.
  276:     %% currently max selection size is 100 domains.
  277:     NumOfDomains = 1234,
  278:     NumOfDomainsToRemove = 1234 div 4,
  279:     NewDomains = [<<"dummy_domain_", (integer_to_binary(N))/binary, ".localhost">>
  280:                   || N <- lists:seq(1, NumOfDomains)],
  281:     [mongoose_domain_core:insert(Domain, ?DYNAMIC_HOST_TYPE1, dummy_src)
  282:      || Domain <- NewDomains],
  283:     meck:new(mongoose_domain_core, [passthrough]),
  284:     WrapperFn = make_wrapper_fn(NumOfDomainsToRemove * 2, NumOfDomainsToRemove),
  285:     meck:expect(mongoose_domain_core, for_each_domain,
  286:                 fun(HostType, Fn) ->
  287:                     meck:passthrough([HostType, WrapperFn(Fn)])
  288:                 end),
  289:     Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"),
  290:     Handler = mongoose_packet_handler:new(?MODULE),
  291:     %% Note that mongoose_domain_core:for_each_domain/2 is used to register subdomain.
  292:     %% some domains are removed during subdomain registration, see make_wrapper_fn/2
  293:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  294:                                                                 Pattern1, Handler)),
  295:     mongoose_subdomain_core:sync(),
  296:     %% try to add some domains second time, as this is also possible during
  297:     %% subdomain registration
  298:     AllDomains = mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1),
  299:     [RegisteredDomain1, RegisteredDomain2 | _] = AllDomains,
  300:     mongoose_subdomain_core:add_domain(?DYNAMIC_HOST_TYPE1, RegisteredDomain1),
  301:     mongoose_subdomain_core:add_domain(?DYNAMIC_HOST_TYPE1, RegisteredDomain2),
  302:     %% and finally try to remove some domains second time
  303:     RemovedDomains = NewDomains -- AllDomains,
  304:     [RemovedDomain1, RemovedDomain2 | _] = RemovedDomains,
  305:     mongoose_subdomain_core:remove_domain(?DYNAMIC_HOST_TYPE1, RemovedDomain1),
  306:     mongoose_subdomain_core:remove_domain(?DYNAMIC_HOST_TYPE1, RemovedDomain2),
  307:     Subdomains = get_all_subdomains(),
  308:     ?assertEqual(NumOfDomains - NumOfDomainsToRemove, length(Subdomains)),
  309:     AllExpectedSubDomains = [mongoose_subdomain_utils:get_fqdn(Pattern1, Domain)
  310:                              || Domain <- AllDomains],
  311:     ?assertEqualLists(AllExpectedSubDomains, Subdomains),
  312:     ?assertEqual(NumOfDomainsToRemove,
  313:                  meck:num_calls(mongoose_lazy_routing, maybe_remove_subdomain, 1)),
  314:     RemovedSubdomains = [mongoose_subdomain_utils:get_fqdn(Pattern1, Domain)
  315:                          || Domain <- RemovedDomains],
  316:     ?assertEqualLists(RemovedSubdomains, get_list_of_disabled_subdomains()),
  317:     no_collisions(),
  318:     meck:unload(mongoose_domain_core).
  319: 
  320: prevents_double_subdomain_registration(_Config) ->
  321:     Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"),
  322:     Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.fqdn"),
  323:     Handler = mongoose_packet_handler:new(?MODULE),
  324:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  325:                                                                 Pattern1, Handler)),
  326:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  327:                                                                 Pattern2, Handler)),
  328:     ?assertEqual({error, already_registered},
  329:                  mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  330:                                                             Pattern1, Handler)),
  331:     ?assertEqual({error, already_registered},
  332:                  mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  333:                                                             Pattern2, Handler)).
  334: 
  335: %%-------------------------------------------------------------------------------------
  336: %% test cases for subdomain names collisions.
  337: %%-------------------------------------------------------------------------------------
  338: %% There are three possible subdomain names collisions:
  339: %%   1) Different domain/subdomain_pattern pairs produce one and the same subdomain.
  340: %%   2) Attempt to register the same FQDN subdomain for 2 different host types.
  341: %%   3) Domain/subdomain_pattern pair produces the same subdomain name as another
  342: %%      FQDN subdomain.
  343: %%
  344: %% Collisions of the first type can eliminated by allowing only one level subdomains,
  345: %% e.g. ensuring that subdomain template corresponds to this regex "^[^.]*\.@HOST@$".
  346: %%
  347: %% Collisions of the second type are less critical as they can be detected during
  348: %% init phase - they result in {error, subdomain_already_exists} return code, so
  349: %% modules can detect it and crash at ?MODULE:start/2.
  350: %%
  351: %% Third type is hard to resolve in automatic way. One of the options is to ensure
  352: %% that FQDN subdomains don't start with the same "prefix" as subdomain patterns.
  353: %%
  354: %% It's good idea to create a metric for such collisions, so devops can set some
  355: %% alarm and react on it.
  356: %%
  357: %% The current behaviour rejects insertion of the conflicting subdomain, the original
  358: %% subdomain must remain unchanged
  359: %%-------------------------------------------------------------------------------------
  360: prevents_prefix_subdomain_overriding_by_prefix_subdomain(_Config) ->
  361:     Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("sub.@HOST@"),
  362:     Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("sub.domain.@HOST@"),
  363:     Handler = mongoose_packet_handler:new(?MODULE, #{host_type => dummy_type}),
  364:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  365:                                                                 Pattern1, Handler)),
  366:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  367:                                                                 Pattern2, Handler)),
  368:     %% one prefix subdomain conflicts with another prefix subdomain
  369:     mongoose_domain_core:insert(<<"test">>, ?DYNAMIC_HOST_TYPE1, dummy_src),
  370:     mongoose_domain_core:insert(<<"domain.test">>, ?DYNAMIC_HOST_TYPE1, dummy_src),
  371:     ?assertEqualLists(
  372:         [<<"sub.domain.domain.test">>, <<"sub.domain.test">>, <<"sub.test">>],
  373:         get_all_subdomains()),
  374:     ?assertEqualLists([<<"test">>, <<"domain.test">>],
  375:                       mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)),
  376:     %% "test" domain is added first, so subdomain for this domain must remain unchanged
  377:     ExpectedSubdomainInfo =
  378:         #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern2,
  379:           parent_domain => <<"test">>, packet_handler => Handler,
  380:           subdomain => <<"sub.domain.test">>},
  381:     ?assertEqual({ok, ExpectedSubdomainInfo},
  382:                  mongoose_subdomain_core:get_subdomain_info(<<"sub.domain.test">>)),
  383:     ?assertEqual([#{what => subdomains_collision, subdomain => <<"sub.domain.test">>}],
  384:                  get_list_of_subdomain_collisions()),
  385:     no_domain_collisions(),
  386:     meck:reset(mongoose_subdomain_core),
  387:     %% check that removal of "domain.test" domain doesn't affect
  388:     %% "sub.domain.test" subdomain
  389:     mongoose_domain_core:delete(<<"domain.test">>),
  390:     ?assertEqual([<<"test">>],
  391:                  mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)),
  392:     ?assertEqualLists([<<"sub.domain.test">>, <<"sub.test">>], get_all_subdomains()),
  393:     ?assertEqual({ok, ExpectedSubdomainInfo},
  394:                  mongoose_subdomain_core:get_subdomain_info(<<"sub.domain.test">>)),
  395:     ?assertEqual([<<"sub.domain.domain.test">>], get_list_of_disabled_subdomains()),
  396:     no_collisions().
  397: 
  398: prevents_fqdn_subdomain_overriding_by_prefix_subdomain(_Config) ->
  399:     Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"),
  400:     Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.fqdn"),
  401:     Handler = mongoose_packet_handler:new(?MODULE, #{host_type => dummy_type}),
  402:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  403:                                                                 Pattern1, Handler)),
  404:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  405:                                                                 Pattern2, Handler)),
  406:     %% FQDN subdomain conflicts with prefix subdomain
  407:     mongoose_domain_core:insert(<<"fqdn">>, ?DYNAMIC_HOST_TYPE1, dummy_src),
  408:     ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()),
  409:     ?assertEqual([<<"fqdn">>],
  410:                  mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)),
  411:     %% FQDN subdomain is added first, so it must remain unchanged
  412:     ExpectedSubdomainInfo =
  413:         #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern2,
  414:           parent_domain => no_parent_domain, packet_handler => Handler,
  415:           subdomain => <<"subdomain.fqdn">>},
  416:     ?assertEqual({ok, ExpectedSubdomainInfo},
  417:                  mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)),
  418:     ?assertEqual([#{what => subdomains_collision, subdomain => <<"subdomain.fqdn">>}],
  419:                  get_list_of_subdomain_collisions()),
  420:     no_domain_collisions(),
  421:     meck:reset(mongoose_subdomain_core),
  422:     %% check that removal of "fqdn" domain doesn't affect FQDN subdomain
  423:     mongoose_domain_core:delete(<<"fqdn">>),
  424:     ?assertEqual([], mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)),
  425:     ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()),
  426:     ?assertEqual({ok, ExpectedSubdomainInfo},
  427:                  mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)),
  428:     no_collisions().
  429: 
  430: prevents_fqdn_subdomain_overriding_by_fqdn_subdomain(_Config) ->
  431:     Pattern = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.fqdn"),
  432:     Handler = mongoose_packet_handler:new(?MODULE, #{host_type => dummy_type}),
  433:     %% FQDN subdomain conflicts with another FQDN subdomain
  434:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  435:                                                                 Pattern, Handler)),
  436:     ?assertEqual({error, subdomain_already_exists},
  437:                  mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2,
  438:                                                             Pattern, Handler)),
  439:     %% FQDN subdomain for ?DYNAMIC_HOST_TYPE1 is registered first, so it must
  440:     %% remain unchanged
  441:     ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()),
  442:     ExpectedSubdomainInfo =
  443:         #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern,
  444:           parent_domain => no_parent_domain, packet_handler => Handler,
  445:           subdomain => <<"subdomain.fqdn">>},
  446:     ?assertEqual({ok, ExpectedSubdomainInfo},
  447:                  mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)),
  448:     ?assertEqual([#{what => subdomains_collision, subdomain => <<"subdomain.fqdn">>}],
  449:                  get_list_of_subdomain_collisions()),
  450:     no_domain_collisions(),
  451:     meck:reset(mongoose_subdomain_core),
  452:     %% check that unregistering FQDN subdomain for ?DYNAMIC_HOST_TYPE2 doesn't
  453:     %% affect FQDN subdomain for ?DYNAMIC_HOST_TYPE1
  454:     ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2,
  455:                                                                   Pattern)),
  456:     ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()),
  457:     ?assertEqual({ok, ExpectedSubdomainInfo},
  458:                  mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)),
  459:     no_collisions().
  460: 
  461: prevents_prefix_subdomain_overriding_by_fqdn_subdomain(_Config) ->
  462:     Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"),
  463:     Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.fqdn"),
  464:     Handler = mongoose_packet_handler:new(?MODULE, #{host_type => dummy_type}),
  465:     %% FQDN subdomain conflicts with another FQDN subdomain
  466:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  467:                                                                 Pattern1, Handler)),
  468:     mongoose_domain_core:insert(<<"fqdn">>, ?DYNAMIC_HOST_TYPE1, dummy_src),
  469:     ?assertEqual({error, subdomain_already_exists},
  470:                  mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  471:                                                             Pattern2, Handler)),
  472:     %% FQDN subdomain for ?DYNAMIC_HOST_TYPE1 is registered first, so it must
  473:     %% remain unchanged
  474:     ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()),
  475:     ExpectedSubdomainInfo =
  476:         #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern1,
  477:           parent_domain => <<"fqdn">>, packet_handler => Handler,
  478:           subdomain => <<"subdomain.fqdn">>},
  479:     ?assertEqual({ok, ExpectedSubdomainInfo},
  480:                  mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)),
  481:     ?assertEqual([#{what => subdomains_collision, subdomain => <<"subdomain.fqdn">>}],
  482:                  get_list_of_subdomain_collisions()),
  483:     no_domain_collisions(),
  484:     meck:reset(mongoose_subdomain_core),
  485:     %% check that unregistering FQDN subdomain for ?DYNAMIC_HOST_TYPE2 doesn't
  486:     %% affect FQDN subdomain for ?DYNAMIC_HOST_TYPE1
  487:     ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE1,
  488:                                                                   Pattern2)),
  489:     ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()),
  490:     ?assertEqual({ok, ExpectedSubdomainInfo},
  491:                  mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)),
  492:     no_collisions().
  493: 
  494: 
  495: %%-------------------------------------------------------------------------------------
  496: %% test cases for domain/subdomain names collisions.
  497: %%-------------------------------------------------------------------------------------
  498: %% There are two possible domain/subdomain names collisions:
  499: %%   1) Domain/subdomain_pattern pair produces the same subdomain name as another
  500: %%      existing top level domain
  501: %%   2) FQDN subdomain is the same as some registered top level domain
  502: %%
  503: %% The naive domain/subdomain registration rejection is probably a bad option:
  504: %%   * Domains and subdomains ETS tables are managed asynchronously, in addition to
  505: %%     that subdomains patterns registration is done async as well. This all leaves
  506: %%     room for various race conditions if we try to just make a verification and
  507: %%     prohibit domain/subdomain registration in case of any collisions.
  508: %%   * The only way to avoid such race conditions is to block all async. ETSs
  509: %%     editing during the validation process, but this can result in big delays
  510: %%     during the MIM initialisation phase.
  511: %%   * Also it's not clear how to interpret registration of the "prefix" based
  512: %%     subdomain patterns, should we block the registration of the whole pattern
  513: %%     or just only conflicting subdomains registration. Blocking of the whole
  514: %%     pattern requires generation and verification of all the subdomains (with
  515: %%     ETS blocking during that process), which depends on domains ETS size and
  516: %%     might take too long.
  517: %%   * And the last big issue with simple registration rejection approach, different
  518: %%     nodes in the cluster might have different registration sequence. So we may
  519: %%     end up in a situation when some nodes registered domain name as a subdomain,
  520: %%     while other nodes registered it as a top level domain.
  521: %%
  522: %% The better way is to prohibit registration of a top level domain if it is equal
  523: %% to any of the FQDN subdomains or if beginning of domain name matches the prefix
  524: %% of any subdomain template. In this case we don't need to verify subdomains at all,
  525: %% verification of domain names against some limited number of subdomains patterns is
  526: %% enough. And the only problem that we need to solve - mongooseim_domain_core must
  527: %% be aware of all the subdomain patterns before it registers the first dynamic
  528: %% domain. This would require minor configuration rework, e.g. tracking of subdomain
  529: %% templates preprocessing (mongoose_subdomain_utils:make_subdomain_pattern/1 calls)
  530: %% during TOML config parsing.
  531: %%
  532: %% It's good idea to create a metric for such collisions, so devops can set some
  533: %% alarm and react on it.
  534: %%
  535: %% The current behaviour just ensures detection of the domain/subdomain names
  536: %% collision, both (domain and subdomain) records remain unchanged in the
  537: %% corresponding ETS tables
  538: %%-------------------------------------------------------------------------------------
  539: detects_domain_conflict_with_prefix_subdomain(_Config) ->
  540:     Pattern = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"),
  541:     Handler = mongoose_packet_handler:new(?MODULE),
  542:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  543:                                                                 Pattern, Handler)),
  544:     mongoose_domain_core:insert(<<"test.net">>, ?DYNAMIC_HOST_TYPE1, dummy_src),
  545:     %% without this sync call "subdomain.example.net" collision can be detected
  546:     %% twice, one time by check_subdomain_name/1 function and then second time
  547:     %% by check_domain_name/2.
  548:     mongoose_subdomain_core:sync(),
  549:     mongoose_domain_core:insert(<<"subdomain.test.net">>, ?DYNAMIC_HOST_TYPE2, dummy_src),
  550:     mongoose_domain_core:insert(<<"subdomain.test.org">>, ?DYNAMIC_HOST_TYPE2, dummy_src),
  551:     mongoose_domain_core:insert(<<"test.org">>, ?DYNAMIC_HOST_TYPE1, dummy_src),
  552:     ?assertEqualLists([<<"subdomain.test.org">>, <<"subdomain.test.net">>],
  553:                       get_all_subdomains()),
  554:     ?assertEqualLists(
  555:         [<<"subdomain.test.org">>, <<"subdomain.test.net">> | ?DYNAMIC_DOMAINS],
  556:         mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE2)),
  557:     no_subdomain_collisions(),
  558:     ?assertEqual(
  559:         [#{what => check_domain_name_failed, domain => <<"subdomain.test.net">>},
  560:          #{what => check_subdomain_name_failed, subdomain => <<"subdomain.test.org">>}],
  561:         get_list_of_domain_collisions()),
  562:     ?assertEqual([<<"subdomain.test.net">>], get_list_of_disabled_subdomains()).
  563: 
  564: detects_domain_conflict_with_fqdn_subdomain(_Config) ->
  565:     Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"),
  566:     Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("another.fqdn"),
  567:     Handler = mongoose_packet_handler:new(?MODULE),
  568: 
  569:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  570:                                                                 Pattern1, Handler)),
  571:     mongoose_domain_core:insert(<<"some.fqdn">>, ?DYNAMIC_HOST_TYPE1, dummy_src),
  572:     mongoose_domain_core:insert(<<"another.fqdn">>, ?DYNAMIC_HOST_TYPE1, dummy_src),
  573:     ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1,
  574:                                                                 Pattern2, Handler)),
  575:     ?assertEqualLists([<<"some.fqdn">>, <<"another.fqdn">>], get_all_subdomains()),
  576:     ?assertEqualLists([<<"some.fqdn">>, <<"another.fqdn">>],
  577:                       mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)),
  578:     no_subdomain_collisions(),
  579:     ?assertEqual(
  580:         [#{what => check_domain_name_failed, domain => <<"some.fqdn">>},
  581:          #{what => check_subdomain_name_failed, subdomain => <<"another.fqdn">>}],
  582:         get_list_of_domain_collisions()),
  583:     ?assertEqual([<<"some.fqdn">>], get_list_of_disabled_subdomains()).
  584: 
  585: %%-------------------------------------------------------------------
  586: %% internal functions
  587: %%-------------------------------------------------------------------
  588: setup_meck(TestCase) ->
  589:     meck:new(mongoose_lazy_routing, [no_link]),
  590:     meck:new(mongoose_subdomain_core, [no_link, passthrough]),
  591:     meck:expect(mongoose_lazy_routing, maybe_remove_domain, fun(_, _) -> ok end),
  592:     RemoveSubdomainFn =
  593:     if
  594:         detects_domain_conflict_with_prefix_subdomain =:= TestCase;
  595:         detects_domain_conflict_with_fqdn_subdomain =:= TestCase ->
  596:             %% Subdomain should never overshadow top level domain name.
  597:             %% In case of conflict with domain name, we want to remove
  598:             %% subdomain routing and IQ handling, but keep ETS record
  599:             %% of that subdomain for troubleshooting.
  600:             fun(_) -> ?assertEqual(whereis(mongoose_subdomain_core), self()) end;
  601:         true ->
  602:             fun(SubdomainInfo) ->
  603:                 %% For all other cases ensure that subdomain is removed from
  604:                 %% the ETS table before mongoose_lazy_routing module notified
  605:                 %% about it
  606:                 Subdomain = maps:get(subdomain, SubdomainInfo),
  607:                 ?assertEqual({error, not_found},
  608:                              mongoose_subdomain_core:get_host_type(Subdomain)),
  609:                 ?assertEqual(whereis(mongoose_subdomain_core), self())
  610:             end
  611:     end,
  612:     meck:expect(mongoose_lazy_routing, maybe_remove_subdomain, RemoveSubdomainFn).
  613: 
  614: get_all_subdomains() ->
  615:     mongoose_subdomain_core:sync(),
  616:     get_subdomains().
  617: 
  618: get_subdomains() ->
  619:     %% mongoose_subdomain_core table is indexed by subdomain name field
  620:     KeyPos = ets:info(mongoose_subdomain_core, keypos),
  621:     [element(KeyPos, Item) || Item <- ets:tab2list(mongoose_subdomain_core)].
  622: 
  623: make_wrapper_fn(N, M) when N > M ->
  624:     %% the wrapper function generates a new loop processing function
  625:     %% that pauses after after processing N domains, removes M of the
  626:     %% already processed domains and resumes after that.
  627:     fun(Fn) ->
  628:         put(number_of_iterations, 0),
  629:         fun(HostType, DomainName) ->
  630:             NumberOfIterations = get(number_of_iterations),
  631:             if
  632:                 NumberOfIterations =:= N -> remove_some_domains(M);
  633:                 true -> ok
  634:             end,
  635:             put(number_of_iterations, NumberOfIterations + 1),
  636:             Fn(HostType, DomainName)
  637:         end
  638:     end.
  639: 
  640: remove_some_domains(N) ->
  641:     AllSubdomains = get_subdomains(),
  642:     [begin
  643:          {ok, Info} = mongoose_subdomain_core:get_subdomain_info(Subdomain),
  644:          ParentDomain = maps:get(parent_domain, Info),
  645:          mongoose_domain_core:delete(ParentDomain)
  646:      end || Subdomain <- lists:sublist(AllSubdomains, N)].
  647: 
  648: no_collisions() ->
  649:     no_domain_collisions(),
  650:     no_subdomain_collisions().
  651: 
  652: no_domain_collisions() ->
  653:     Hist = meck:history(mongoose_subdomain_core),
  654:     Errors = [Call || {_P, {_M, log_error = _F, [From, _] = _A}, _R} = Call <- Hist,
  655:                       From =:= check_subdomain_name orelse From =:= check_domain_name],
  656:     ?assertEqual([], Errors).
  657: 
  658: get_list_of_domain_collisions() ->
  659:     Hist = meck:history(mongoose_subdomain_core),
  660:     [Error || {_Pid, {_Mod, log_error = _Func, [From, Error] = _Args}, _Result} <- Hist,
  661:               From =:= check_subdomain_name orelse From =:= check_domain_name].
  662: 
  663: no_subdomain_collisions() ->
  664:     Hist = meck:history(mongoose_subdomain_core),
  665:     Errors = [Call || {_P, {_M, log_error = _F, [From, _] = _A}, _R} = Call <- Hist,
  666:                       From =:= report_subdomains_collision],
  667:     ?assertEqual([], Errors).
  668: 
  669: get_list_of_subdomain_collisions() ->
  670:     Hist = meck:history(mongoose_subdomain_core),
  671:     [Error || {_Pid, {_Mod, log_error = _Func, [From, Error] = _Args}, _Result} <- Hist,
  672:               From =:= report_subdomains_collision].
  673: 
  674: get_list_of_disabled_subdomains() ->
  675:     History = meck:history(mongoose_lazy_routing),
  676:     [maps:get(subdomain, Info)
  677:      || {_Pid, {_Mod, Func, [Info] = _Args}, _Result} <- History,
  678:         Func =:= maybe_remove_subdomain].