diff --git a/.gitignore b/.gitignore index 49cd4e3..c5e740a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,19 @@ *.beam .rebar erl_crash.dump ebin deps db log .*.swp Mnesia* test.spec cover.spec test_logs rel/ _build rebar.lock +/ct_* +/*.html +/jquery* +/variables-ct* diff --git a/CHANGELOG.md b/CHANGELOG.md index 66119fd..a328559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,70 +1,75 @@ # Changelog All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added ### Changed ### Deprecated ### Removed ### Fixed ### Security +## [0.9.3] - 2017-05-03 +### Fixed +- With a properly crafted folder list, clients could receive empty lines + in filtered folder listings + ## [0.9.2] - 2017-03-21 ### Fixed - Fix client message processing when there are no active rules ## [0.9.1] - 2016-02-20 ### Fixed - Improve SSL connection accepts (prevent timing errors on the socket) - Support fragmentary messages from clients (e.g. tag in one packet, cmd in another) ## [0.9.0] - 2016-07-29 ### Added - bind to a network interface (rather than an IP/host) with net_iface ### Changed - handle the implicit ssl upgrade a bit more manually, allowing faster replenish of the listener pool and simplifying socket setup code - upgraded build to rebar3 - Upgraded eimap to 0.3.0 ### Fixed - fix CAPABILITY response (was CAPABILITIES) ## [0.8.3] - 2016-08-08 ### Fixed - always close ssl sockets as an ssl socket - keep listen_socket separate from socket - not required to make the socket active to accept connections - ensure eimap processes are always terminated in all cases ## [0.8.2] - 2016-07-08 ### Added - listener_pool_size configuration option for listeners ### Changed - Default size of listener pool drops to 10 from 20 - Rate limit (by introducing a short wait) connection accept()s ### Fixed - Prevent starvation of the session pool due to clients dropping connections pre-accept() ## [0.8.1] - 2016-07-06 ### Added - ipv6 connections ### Changed - update to eimap 0.2.5 ### Fixed - Ignore non-listing LIST commands (e.g. requests for the root/separator) - Tidy up the server greetings ## [0.8.0] - 2016-06-08 ### Added - systemd service module - sysv init script ### Changed - Upgraded eimap to 0.2.4 ### Fixed - Support more variations of the LIST command args in the filter_groupware rule diff --git a/Makefile b/Makefile index 5822ae6..63cde4b 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,22 @@ REBAR = $(shell which rebar3 || echo ./rebar3) ENABLE_STATIC = no all: ENABLE_STATIC=$(ENABLE_STATIC) $(REBAR) compile clean: $(REBAR) clean run: $(REBAR) shell --config app.config release: $(REBAR) release test.spec: test.spec.in cover.spec.in cat test.spec.in | sed -e "s,@PATH@,$(PWD)," > $(PWD)/test.spec cat cover.spec.in | sed -e "s,@PATH@,$(PWD)," > $(PWD)/cover.spec -test: test.spec guam - mkdir -p test_logs - ct_run -pa $(PWD)/apps/*/ebin -pa $(PWD)/deps/*/ebin -spec test.spec -erl_args -config $(PWD)/apps/kolab_guam/test/test.config -s kolab_guam +test: test.spec all + $(REBAR) ct --cover --readable --allow_user_terms diff --git a/apps/kolab_guam/src/kolab_guam.app.src b/apps/kolab_guam/src/kolab_guam.app.src index 83e9056..e879d29 100644 --- a/apps/kolab_guam/src/kolab_guam.app.src +++ b/apps/kolab_guam/src/kolab_guam.app.src @@ -1,20 +1,20 @@ %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*- {application, kolab_guam, [ {description, "IMAP session proxy"}, - {vsn, "0.9.2"}, + {vsn, "0.9.3"}, {registered, []}, {applications, [ kernel, stdlib, compiler, syntax_tools, goldrush, lager, crypto, ssl ]}, {mod, { kolab_guam, []}}, {env, [ ]} ]}. diff --git a/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl b/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl index 4400f31..decbd9a 100644 --- a/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl +++ b/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl @@ -1,140 +1,149 @@ %% Copyright 2015 Kolab Systems AG (http://www.kolabsys.com) %% %% Aaron Seigo (Kolab Systems) %% %% This program is free software: you can redistribute it and/or modify %% it under the terms of the GNU General Public License as published by %% the Free Software Foundation, either version 3 of the License, or %% (at your option) any later version. %% %% This program is distributed in the hope that it will be useful, %% but WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %% GNU General Public License for more details. %% %% You should have received a copy of the GNU General Public License %% along with this program. If not, see . -module(kolab_guam_rule_filter_groupware). -export([new/1, applies/4, imap_data/3, apply_to_client_message/4, apply_to_server_message/3]). -behavior(kolab_guam_rule). --record(state, { blacklist = [], tag = <<>>, active = false, last_chunk = <<>>, - trigger_commands = [<<"LIST">>, <<"list">>, <<"XLIST">>, <<"xlist">>, <<"LSUB">>, <<"lsub">>]}). +-include("kolab_guam_rule_filter_groupware.hrl"). new(_Config) -> #state { blacklist = undefined }. applies(_ConnectionDetails, _Buffer, { _Tag, Command, Data }, State) -> Applies = apply_if_id_matches(Command, Data, State), %lager:debug("********** Checking ...~n Command: ~s ~s, Result ~p", [Command, Data, Applies]), { Applies, State }. +apply_to_client_message(_ImapSession, Buffer, undefined, State) -> + { Buffer, State }; + apply_to_client_message(ImapSession, Buffer, { Tag, Command, Data }, State) -> { Active, StateTag } = case is_triggering_command(Command, Data, State) of true -> fetch_metadata(ImapSession, State), { true, Tag }; _ -> { false, <<>> } end, %lager:info("Client sent: ~s ~s ~p", [Command, Data, Active]), { Buffer, State#state{ active = Active, tag = StateTag }}. apply_to_server_message(_ImapSession, Buffer, #state{ active = true } = State) -> filter_folders(Buffer, State); apply_to_server_message(_ImapSession, Buffer, State) -> { Buffer, State }. imap_data(blacklist, { error, _Reason }, State) -> State; imap_data(blacklist, Response, State) -> %TODO: we don't need Foo/Bar if we already have Foo, so filter folders-of-groupwarefolders Blacklist = lists:foldl(fun({ _Folder, [ { _Property, null } ]}, Acc) -> Acc; ({ _Folder, [ { _Property, <<"mail", _Rest/binary>> } ]}, Acc) -> Acc; ({ Folder, _ }, Acc) -> [{ Folder, <> }|Acc] end, [], Response), State#state{ blacklist = Blacklist }. %%PRIVATE is_triggering_command(Command, Data, #state{ trigger_commands = TriggerCommands }) -> %% if the command is in the list of trigger commands and the ending is not "" (which means "send me %% the root and separator" according to RFC 3501), then it is treated as a triggering event lists:any(fun(T) -> (Command =:= T) andalso (binary:longest_common_suffix([Data, <<"\"\"">>]) =/= 2) end, TriggerCommands). fetch_metadata(none, #state{ blacklist = undefined }) -> ok; fetch_metadata(ImapSession, #state{ blacklist = undefined }) -> eimap:get_folder_metadata(ImapSession, self(), { rule_data, ?MODULE, blacklist }, "*", ["/shared/vendor/kolab/folder-type"]); fetch_metadata(_ImapSession, _State) -> ok. apply_if_id_matches(<<"ID">>, Data, _State) -> apply_if_found_kolab(string:str(string:to_lower(binary_to_list(Data)), "/kolab")); apply_if_id_matches(Command, Data, State) -> case is_triggering_command(Command, Data, State) of true -> true; _ -> notyet end. apply_if_found_kolab(0) -> true; apply_if_found_kolab(_) -> false. +possibly_append_newline(<<>>) -> <<>> ; +possibly_append_newline(Response) -> <> . + filter_folders(<<>>, State) -> { <<>>, State#state{ active = true } }; filter_folders(Buffer, #state{ last_chunk = LeftOvers } = State) -> + % Add the left overs from the previous buffer to the current buffer FullBuffer = <>, + % From that buffer, only take the complete lines and save off the remainder. { FullLinesBuffer, LastChunk } = eimap_utils:only_full_lines(FullBuffer), + % Create a list so we can filter the individual folders ListResponses = binary:split(FullLinesBuffer, <<"\r\n">>, [ global ]), { Response, More } = filter_folders(State, ListResponses, { <<>>, true }), %io:format("Filtered ... ~p~n", [Response]), - { <>, State#state { active = More, last_chunk = LastChunk } }. + %Note that the last list item does not contain \r\n, so that needs to be added unless we filtered the complete content. + { possibly_append_newline(Response), State#state { active = More, last_chunk = LastChunk } }. filter_folders(_State, [], Return) -> Return; filter_folders(_State, _Folders, { Acc, false }) -> { Acc, false }; filter_folders(State, [Unfiltered|Folders], { Acc, _More }) -> filter_folders(State, Folders, filter_folder(State, Unfiltered, Acc)). filter_folder(_State, <<>>, Acc) -> { Acc, true }; filter_folder(State, <<"* LIST ", Details/binary>> = Response, Acc) -> { filter_on_details(State, Response, Acc, Details), true }; filter_folder(State, <<"* XLIST ", Details/binary>> = Response, Acc) -> { filter_on_details(State, Response, Acc, Details), true }; filter_folder(State, <<"* LSUB ", Details/binary>> = Response, Acc) -> { filter_on_details(State, Response, Acc, Details), true }; filter_folder(#state{ tag = Tag }, Response, Acc) -> HasMore = case byte_size(Tag) =< byte_size(Response) of true -> case binary:match(Response, Tag, [{ scope, { 0, byte_size(Tag) } }]) of nomatch -> true; _ -> false % we have found our closing tag! end; false -> true end, { add_response(Response, Acc), HasMore }. filter_on_details(#state{ blacklist = Blacklist }, Response, Acc, Details) -> %% first determine if we have a quoted item or a non-quoted item and start from there DetailsSize = byte_size(Details), { Quoted, Start } = case binary:at(Details, DetailsSize - 1) of $" -> { quoted, DetailsSize - 2 }; _ -> { unquoted, DetailsSize - 1 } end, Folder = find_folder_name(Details, Quoted, Start, Start, binary:at(Details, Start)), %io:format("COMPARING ~p ??? ~p~n", [Folder, in_blacklist(Folder, Blacklist)]), case in_blacklist(Folder, Blacklist) of true -> Acc; _ -> add_response(Response, Acc) end. find_folder_name(Details, quoted, End, Start, $") -> binary:part(Details, Start + 1, End - Start); find_folder_name(Details, unquoted, End, Start, $ ) -> binary:part(Details, Start + 1, End - Start); find_folder_name(Details, _Quoted, _End, 0, _) -> Details; find_folder_name(Details, Quoted, End, Start, _) -> find_folder_name(Details, Quoted, End, Start - 1, binary:at(Details, Start - 1)). add_response(Response, <<>>) -> Response; add_response(Response, Acc) -> <>. in_blacklist(_Folder, undefined) -> false; in_blacklist(_Folder, []) -> false; in_blacklist(Folder, [{ Literal, Prefix }|List]) -> case Literal =:= Folder of true -> true; _ -> case binary:match(Folder, Prefix) of { 0, _ } -> true; _ -> in_blacklist(Folder, List) end end. diff --git a/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.hrl b/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.hrl new file mode 100644 index 0000000..5b1ae0c --- /dev/null +++ b/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.hrl @@ -0,0 +1,3 @@ +-record(state, { blacklist = [], tag = <<>>, active = false, last_chunk = <<>>, + trigger_commands = [<<"LIST">>, <<"list">>, <<"XLIST">>, <<"xlist">>, <<"LSUB">>, <<"lsub">>]}). + diff --git a/apps/kolab_guam/test/kolab_guam_rules_SUITE.erl b/apps/kolab_guam/test/kolab_guam_rules_SUITE.erl index b97d180..0f44cdd 100644 --- a/apps/kolab_guam/test/kolab_guam_rules_SUITE.erl +++ b/apps/kolab_guam/test/kolab_guam_rules_SUITE.erl @@ -1,89 +1,150 @@ %% Copyright 2015 Kolab Systems AG (http://www.kolabsys.com) %% %% Aaron Seigo (Kolab Systems) %% %% This program is free software: you can redistribute it and/or modify %% it under the terms of the GNU General Public License as published by %% the Free Software Foundation, either version 3 of the License, or %% (at your option) any later version. %% %% This program is distributed in the hope that it will be useful, %% but WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %% GNU General Public License for more details. %% %% You should have received a copy of the GNU General Public License %% along with this program. If not, see . -module(kolab_guam_rules_SUITE). % easier than exporting by name -compile(export_all). % required for common_test to work --include("ct.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include("../src/rules/kolab_guam_rule_filter_groupware.hrl"). %%%%%%%%%%%%%%%%%%%%%%%%%%% %% common test callbacks %% %%%%%%%%%%%%%%%%%%%%%%%%%%% % Specify a list of all unit test functions all() -> [ kolab_guam_rule_filter_groupware_responsefiltering_test, kolab_guam_rule_filter_groupware_responsefiltering_multipacket_test ]. % required, but can just return Config. this is a suite level setup function. init_per_suite(Config) -> Config. % required, but can just return Config. this is a suite level tear down function. end_per_suite(Config) -> Config. % optional, can do function level setup for all functions, % or for individual functions by matching on TestCase. init_per_testcase(_TestCase, Config) -> Config. % optional, can do function level tear down for all functions, % or for individual functions by matching on TestCase. end_per_testcase(_TestCase, Config) -> Config. % c("apps/kolab_guam/test/kolab_guam_sup_tests.erl"). eunit:test(kolab_guam_sup_tests). kolab_guam_rule_filter_groupware_responsefiltering_test(_TestConfig) -> + %% Data to be fed into the test, one tuple per iteration + %% Tuple format: { [] = folder_blacklist, input_data, correct_output } Data = [ { + [ + {<<"Calendar">>, <<"Calendar/">>}, + {<<"Calendar/Personal Calendar">>, <<"Calendar/Personal Calendar/">>}, + {<<"Configuration">>, <<"Configuration/">>}, + {<<"Contacts">>, <<"Contacts/">>}, + {<<"Contacts/Personal Contacts">>, <<"Contacts/Personal Contacts/">>}, + {<<"Files">>, <<"Files/">>}, + {<<"Journal">>, <<"Journal/">>}, + {<<"Notes">>, <<"Notes/">>}, + {<<"Tasks">>, <<"Tasks/">>} + ], <<"* LIST (\\Noinferiors \\Subscribed) \"/\" INBOX\r\n* LIST (\\Subscribed) \"/\" Archive\r\n* LIST (\\Subscribed \\HasChildren) \"/\" Calendar\r\n* LIST (\\Subscribed) \"/\" \"Calendar/Personal Calendar\"\r\n* LIST (\\Subscribed) \"/\" Configuration\r\n* LIST (\\Subscribed \\HasChildren) \"/\" Contacts\r\n* LIST (\\Subscribed) \"/\" \"Contacts/Personal Contacts\"\r\n* LIST (\\Subscribed) \"/\" Drafts\r\n* LIST (\\Subscribed) \"/\" Files\r\n* LIST (\\Subscribed) \"/\" Journal\r\n* LIST (\\Subscribed) \"/\" Notes\r\n* LIST (\\Subscribed) \"/\" Sent\r\n* LIST (\\Subscribed) \"/\" Spam\r\n* LIST (\\Subscribed) \"/\" Tasks\r\n* LIST (\\Subscribed) \"/\" Trash\r\n7 OK Completed (0.000 secs 15 calls)\r\n">>, <<"* LIST (\\Noinferiors \\Subscribed) \"/\" INBOX\r\n* LIST (\\Subscribed) \"/\" Archive\r\n* LIST (\\Subscribed) \"/\" Drafts\r\n* LIST (\\Subscribed) \"/\" Sent\r\n* LIST (\\Subscribed) \"/\" Spam\r\n* LIST (\\Subscribed) \"/\" Trash\r\n7 OK Completed (0.000 secs 15 calls)\r\n">> } ], + + %% setup boilerplate Config = {}, %%TODO? State = kolab_guam_rule_filter_groupware:new(Config), ServerConfig = kolab_guam_sup:default_imap_server_config(), { ok, ImapSession } = eimap:start_link(ServerConfig), - { _, ReadyState } = kolab_guam_rule_filter_groupware:apply_to_client_message(ImapSession, <<"7 list (subscribed) \"\" \"*\" return (special-use)">>, State), - lists:foreach(fun({ Input, Filtered }) -> { Filtered, NewState } = kolab_guam_rule_filter_groupware:apply_to_server_message(ImapSession, Input, ReadyState) end, Data). + + %% create the rule, ready for testing + ClientData = <<"7 list (subscribed) \"\" \"*\" return (special-use)">>, + Split = eimap_utils:split_command_into_components(ClientData), + { _, ReadyState } = kolab_guam_rule_filter_groupware:apply_to_client_message(ImapSession, ClientData, Split, State), + + %% run the dataset through the rule + lists:foreach(fun({ Blacklist, Input, Filtered }) -> + BlacklistState = ReadyState#state{ blacklist = Blacklist }, + { Filtered, _NewState } = kolab_guam_rule_filter_groupware:apply_to_server_message(ImapSession, Input, BlacklistState) end, + Data). kolab_guam_rule_filter_groupware_responsefiltering_multipacket_test(_TestConfig) -> + %% Data to be fed into the test, one tuple per iteration + %% Tuple format: { [ input_packets ] = complete_message, correct_output } Data = [ { + [ + {<<"Calendar">>, <<"Calendar/">>}, + {<<"Calendar/Personal Calendar">>, <<"Calendar/Personal Calendar/">>}, + {<<"Configuration">>, <<"Configuration/">>}, + {<<"Contacts">>, <<"Contacts/">>}, + {<<"Contacts/Personal Contacts">>, <<"Contacts/Personal Contacts/">>}, + {<<"Files">>, <<"Files/">>}, + {<<"Journal">>, <<"Journal/">>}, + {<<"Notes">>, <<"Notes/">>}, + {<<"Tasks">>, <<"Tasks/">>} + ], [ <<"* LIST (\\Noinferiors \\Subscribed) \"/\" INBOX\r\n* LIST (\\Subscribed) \"/\" Archive\r\n* LIST (\\Subscribed \\HasChildren) \"/\" Calendar\r\n* LIST (\\Subscribed) \"/\" \"Calendar/Personal Calendar\"\r\n* LIST (\\Subscribed) \"/\" Configuration\r\n* LIST (\\Subscribed \\HasChildren) \"/\" Contacts\r\n* LIST (\\Subscribed) \"/\" \"Contacts/Personal Contacts\"\r\n* LIST (\\Subscribed) \"/\" Drafts\r\n* LIST (\\Subscribed) \"/\" Files\r\n* LIST (\\Subscribed) \"/\" Journal\r\n* LIST (\\Subscribed)">>, <<"\"/\" Notes\r\n* LIST (\\Subscribed) \"/\" Sent\r\n* LIST (\\Subscribed) \"/\" Spam\r\n* LIST (\\Subscribed) \"/\" Tasks\r\n* LIST (\\Subscribed) \"/\" Trash\r\n7 OK Completed (0.000 secs 15 calls)\r\n">> ], <<"* LIST (\\Noinferiors \\Subscribed) \"/\" INBOX\r\n* LIST (\\Subscribed) \"/\" Archive\r\n* LIST (\\Subscribed) \"/\" Drafts\r\n* LIST (\\Subscribed) \"/\" Sent\r\n* LIST (\\Subscribed) \"/\" Spam\r\n* LIST (\\Subscribed) \"/\" Trash\r\n7 OK Completed (0.000 secs 15 calls)\r\n">> + }, + %Filter complete packet + { + [ + {<<"Calendar">>, <<"Calendar/">>} + ], + [ + <<"* LIST (\\Subscribed \\HasChildren) \"/\" Calendar\r\n">>, + <<"7 OK Completed (0.000 secs 15 calls)\r\n">> + ], + <<"7 OK Completed (0.000 secs 15 calls)\r\n">> } ], + %% setup boilerplate Config = {}, %%TODO? State = kolab_guam_rule_filter_groupware:new(Config), - { _, ReadyState } = kolab_guam_rule_filter_groupware:apply_to_client_message(<<"7 list (subscribed) \"\" \"*\" return (special-use)">>, State), - lists:foreach(fun({ Input, Filtered }) -> Filtered = filter_groupware_packets(ReadyState, Input, <<>>) end, Data). + ServerConfig = kolab_guam_sup:default_imap_server_config(), + { ok, ImapSession } = eimap:start_link(ServerConfig), + + %% create the rule, ready for testing + ClientData = <<"7 list (subscribed) \"\" \"*\" return (special-use)">>, + Split = eimap_utils:split_command_into_components(ClientData), + { _, ReadyState } = kolab_guam_rule_filter_groupware:apply_to_client_message(ImapSession, ClientData, Split, State), + + %% run the dataset through the rule + lists:foreach(fun({ Blacklist, Input, Filtered }) -> + BlacklistState = ReadyState#state{ blacklist = Blacklist }, + Filtered = filter_groupware_packets(ImapSession, BlacklistState, Input, <<>>) end, + Data). -filter_groupware_packets(_ReadyState, [], Buffer) -> Buffer; -filter_groupware_packets(ReadyState, [Input|More], Buffer) -> - { Processed, State } = kolab_guam_rule_filter_groupware:apply_to_server_message(Input, ReadyState), - filter_groupware_packets(State, More, <>). +filter_groupware_packets(_ImapSession, _ReadyState, [], Buffer) -> Buffer; +filter_groupware_packets(ImapSession, ReadyState, [Input|More], Buffer) -> + { Processed, State } = kolab_guam_rule_filter_groupware:apply_to_server_message(ImapSession, Input, ReadyState), + filter_groupware_packets(ImapSession, State, More, <>). diff --git a/apps/kolab_guam/test/kolab_guam_sup_SUITE.erl b/apps/kolab_guam/test/kolab_guam_sup_SUITE.erl index c7d6bbc..9504406 100644 --- a/apps/kolab_guam/test/kolab_guam_sup_SUITE.erl +++ b/apps/kolab_guam/test/kolab_guam_sup_SUITE.erl @@ -1,65 +1,78 @@ %% Copyright 2015 Kolab Systems AG (http://www.kolabsys.com) %% %% Aaron Seigo (Kolab Systems) %% %% This program is free software: you can redistribute it and/or modify %% it under the terms of the GNU General Public License as published by %% the Free Software Foundation, either version 3 of the License, or %% (at your option) any later version. %% %% This program is distributed in the hope that it will be useful, %% but WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %% GNU General Public License for more details. %% %% You should have received a copy of the GNU General Public License %% along with this program. If not, see . -module(kolab_guam_sup_SUITE). % easier than exporting by name -compile(export_all). % required for common_test to work --include("ct.hrl"). +-include_lib("common_test/include/ct.hrl"). %%%%%%%%%%%%%%%%%%%%%%%%%%% %% common test callbacks %% %%%%%%%%%%%%%%%%%%%%%%%%%%% % Specify a list of all unit test functions -all() -> [imap_server_config_test, imap_server_settings_to_config_test]. +all() -> [default_imap_server_config_test, imap_server_config_test]. % required, but can just return Config. this is a suite level setup function. init_per_suite(Config) -> + application:set_env(kolab_guam, imap_servers, [ + { default, [ + { host, "192.168.56.102" }, + { port, 994 }, + { tls, true } + ] + }, + { test_default, [ + { host, "192.168.56.101" }, + { port, 993 }, + { tls, false } + ] + } + ] + ), Config. % required, but can just return Config. this is a suite level tear down function. end_per_suite(Config) -> Config. % optional, can do function level setup for all functions, % or for individual functions by matching on TestCase. init_per_testcase(_TestCase, Config) -> Config. % optional, can do function level tear down for all functions, % or for individual functions by matching on TestCase. end_per_testcase(_TestCase, Config) -> Config. -% c("apps/kolab_guam/test/kolab_guam_sup_tests.erl"). eunit:test(kolab_guam_sup_tests). - default_imap_server_config_test(_TestConfig) -> Expected = [ { host, "192.168.56.102" }, { port, 994 }, { tls, true } ], Expected = kolab_guam_sup:default_imap_server_config(). imap_server_config_test(_TestConfig) -> Configs = [ { test_default, [ { host, "192.168.56.101" }, { port, 993 }, { tls, false } ] } ], lists:foreach(fun({ Config, Record }) -> Record = kolab_guam_sup:imap_server_config(Config) end, Configs).