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