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