1 # Copyright (C) 2008-2023 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <https://www.gnu.org/licenses/>.
18 """Mailman test layers."""
20 # XXX 2012-03-23 BAW: Layers really really suck. For example, the
21 # test_owners_get_email() test requires that both the SMTPLayer and LMTPLayer
22 # be set up, but there's apparently no way to do that and make zope.testing
23 # happy. This causes no test failures, but it does cause errors at the end of
24 # the full test run. For now, I'll ignore that, but I do want to eventually
25 # get rid of the layers and use something like testresources or some such.
35 from importlib
.resources
import files
, read_text
36 from lazr
.config
import as_boolean
37 from mailman
.config
import config
38 from mailman
.core
import initialize
39 from mailman
.core
.initialize
import INHIBIT_CONFIG_FILE
40 from mailman
.core
.logging
import get_handler
41 from mailman
.database
.transaction
import transaction
42 from mailman
.interfaces
.domain
import IDomainManager
43 from mailman
.testing
.helpers
import (
49 from mailman
.testing
.mta
import (
50 ConnectionCountingController
,
51 ConnectionCountingSSLController
,
52 ConnectionCountingSTARTTLSController
,
54 from mailman
.utilities
.string
import expand
55 from public
import public
56 from textwrap
import dedent
57 from zope
.component
import getUtility
60 TEST_TIMEOUT
= datetime
.timedelta(seconds
=5)
65 class MockAndMonkeyLayer
:
66 """Layer for mocking and monkey patching for testing."""
68 # Set this to True to enable predictable datetimes, uids, etc.
71 # A registration of all testing factories, for resetting between tests.
75 def testTearDown(cls
):
76 for reset
in cls
._resets
:
80 def register_reset(cls
, reset
):
81 cls
._resets
.append(reset
)
85 class ConfigLayer(MockAndMonkeyLayer
):
86 """Layer for pushing and popping test configurations."""
93 # Set up the basic configuration stuff. Turn off path creation until
94 # we've pushed the testing config.
95 config
.create_paths
= False
96 initialize
.initialize_1(INHIBIT_CONFIG_FILE
)
97 assert cls
.var_dir
is None, 'Layer already set up'
98 # Calculate a temporary VAR_DIR directory so that run-time artifacts
99 # of the tests won't tread on the installation's data. This also
100 # makes it easier to clean up after the tests are done, and insures
101 # isolation of test suite runs.
102 cls
.var_dir
= tempfile
.mkdtemp()
103 # We need a test configuration both for the foreground process and any
104 # child processes that get spawned. lazr.config would allow us to do
105 # it all in a string that gets pushed, and we'll do that for the
106 # foreground, but because we may be spawning processes (such as
107 # runners) we'll need a file that we can specify to the with the -C
108 # option. Craft the full test configuration string here, push it, and
109 # also write it out to a temp file for -C.
111 # Create a dummy postfix.cfg file so that the test suite doesn't try
112 # to run the actual postmap command, which may not exist anyway.
113 postfix_cfg
= os
.path
.join(cls
.var_dir
, 'postfix.cfg')
114 with
open(postfix_cfg
, 'w') as fp
:
117 postmap_command: true
118 transport_file_type: hash
120 test_config
= dedent("""
129 """.format(cls
.var_dir
, postfix_cfg
))
130 # Read the testing config and push it.
131 test_config
+= read_text('mailman.testing', 'testing.cfg')
132 config
.create_paths
= True
133 config
.push('test config', test_config
)
134 # Initialize everything else.
135 initialize
.initialize_2(testing
=True)
136 initialize
.initialize_3()
137 # When stderr debugging is enabled, subprocess root loggers should
138 # also be more verbose.
140 test_config
+= dedent("""
144 # Enable log message propagation and reset the log paths so that the
145 # doctests can check the output.
146 for logger_config
in config
.logger_configs
:
147 sub_name
= logger_config
.name
.split('.')[-1]
148 if sub_name
== 'root':
150 logger_name
= 'mailman.' + sub_name
151 log
= logging
.getLogger(logger_name
)
152 log
.propagate
= cls
.stderr
153 # Reopen the file to a new path that tests can get at. Instead of
154 # using the configuration file path though, use a path that's
155 # specific to the logger so that tests can find expected output
157 path
= os
.path
.join(config
.LOG_DIR
, sub_name
)
158 get_handler(sub_name
).reopen(path
)
159 log
.setLevel(logging
.DEBUG
)
160 # If stderr debugging is enabled, make sure subprocesses are also
161 # more verbose. In general though, we still don't want SQLAlchemy
162 # debugging because it's just too verbose. Unfortunately, if you
163 # do want that level of debugging you currently have to manually
164 # modify this conditional.
165 if sub_name
!= 'database':
166 test_config
+= expand(dedent("""
170 """), None, dict(name
=sub_name
, path
=path
,
171 extra_verbose
=dedent("""
174 """) if cls
.stderr
else ""))
175 # The root logger will already have a handler, but it's not the right
176 # handler. Remove that and set our own.
178 console
= logging
.StreamHandler(sys
.stderr
)
179 formatter
= logging
.Formatter(config
.logging
.root
.format
,
180 config
.logging
.root
.datefmt
)
181 console
.setFormatter(formatter
)
182 root
= logging
.getLogger()
184 root
.addHandler(console
)
185 # Write the configuration file for subprocesses and set up the config
186 # object to pass that properly on the -C option.
187 config_file
= os
.path
.join(cls
.var_dir
, 'test.cfg')
188 with
open(config_file
, 'w') as fp
:
189 fp
.write(test_config
)
191 config
.filename
= config_file
195 assert cls
.var_dir
is not None, 'Layer not set up'
197 # Destroy the test database after the tests are done so that there is
198 # no data in case the tests are rerun with a database layer like mysql
199 # or postgresql which are not deleted in teardown.
200 shutil
.rmtree(cls
.var_dir
)
201 # Prevent the bit of post-processing on the .pop() that creates
202 # directories. We're basically shutting down everything and we don't
203 # need the directories created. Plus, doing so leaves a var directory
204 # turd in the source tree's top-level directory. We do it this way
205 # rather than shutil.rmtree'ing the resulting var directory because
206 # it's possible the user created a valid such directory for
207 # operational or test purposes.
208 config
.create_paths
= False
209 config
.pop('test config')
214 # Add an example domain.
216 getUtility(IDomainManager
).add('example.com', 'An example domain.')
219 def testTearDown(cls
):
222 # Flag to indicate that loggers should propagate to the console.
225 # The top of our source tree, for tests that care (e.g. hooks.txt).
226 root_directory
= None
229 def set_root_directory(cls
, directory
):
230 """Set the directory at the root of our source tree.
232 zc.recipe.testrunner runs from parts/test/working-directory, but
233 that's actually changed over the life of the package. Some tests
234 care, e.g. because they need to find our built-out bin directory.
235 Fortunately, buildout can give us this information. See the
236 `buildout.cfg` file for where this method is called.
238 cls
.root_directory
= directory
242 class SMTPLayer(ConfigLayer
):
243 """Layer for starting, stopping, and accessing a test SMTP server."""
249 assert cls
.smtpd
is None, 'Layer already set up'
250 host
= config
.mta
.smtp_host
251 port
= int(config
.mta
.smtp_port
)
252 cls
.smtpd
= ConnectionCountingController(host
, port
)
257 assert cls
.smtpd
is not None, 'Layer not set up'
263 # Make sure we don't call our superclass's testSetUp(), otherwise the
264 # example.com domain will get added twice.
268 def testTearDown(cls
):
274 class SMTPSLayer(ConfigLayer
):
275 """Layer for starting, stopping, and accessing a test SMTPS server."""
281 assert cls
.smtpd
is None, 'Layer already set up'
282 # Use a different port than the SMTP layer, since that one might
284 config
.push('smtps', """
287 smtp_secure_mode: smtps
289 test_cert_path
= files('mailman.testing') / 'ssl_test_cert.crt'
290 test_key_path
= files('mailman.testing') / 'ssl_test_key.key'
292 client_context
= ssl
.create_default_context()
293 client_context
.load_verify_locations(cafile
=test_cert_path
)
295 server_context
= ssl
.create_default_context(ssl
.Purpose
.CLIENT_AUTH
)
296 server_context
.load_cert_chain(test_cert_path
, test_key_path
)
298 host
= config
.mta
.smtp_host
299 port
= int(config
.mta
.smtp_port
)
301 cls
.smtpd
= ConnectionCountingSSLController(
303 client_context
=client_context
,
304 server_context
=server_context
)
309 # Make sure we don't call our superclass's testSetUp(), otherwise the
310 # example.com domain will get added twice.
314 def testTearDown(cls
):
320 assert cls
.smtpd
is not None, 'Layer not set up'
327 class STARTTLSLayer(ConfigLayer
):
328 """Layer for starting and stopping a test SMTP server with STARTTLS."""
334 assert cls
.smtpd
is None, 'Layer already set up'
335 # Use a different port than the SMTP and SMTPS layers, since that one
336 # might still be in use.
337 config
.push('starttls', """
340 smtp_secure_mode: starttls
342 test_cert_path
= files('mailman.testing') / 'ssl_test_cert.crt'
343 test_key_path
= files('mailman.testing') / 'ssl_test_key.key'
345 client_context
= ssl
.create_default_context()
346 client_context
.load_verify_locations(cafile
=test_cert_path
)
348 server_context
= ssl
.create_default_context(ssl
.Purpose
.CLIENT_AUTH
)
349 server_context
.load_cert_chain(test_cert_path
, test_key_path
)
351 host
= config
.mta
.smtp_host
352 port
= int(config
.mta
.smtp_port
)
354 cls
.smtpd
= ConnectionCountingSTARTTLSController(
356 client_context
=client_context
,
357 server_context
=server_context
)
362 # Make sure we don't call our superclass's testSetUp(), otherwise the
363 # example.com domain will get added twice.
367 def testTearDown(cls
):
373 assert cls
.smtpd
is not None, 'Layer not set up'
376 config
.pop('starttls')
380 class LMTPLayer(ConfigLayer
):
381 """Layer for starting, stopping, and accessing a test LMTP server."""
386 def _wait_for_lmtp_server():
387 get_lmtp_client(quiet
=True)
391 assert cls
.lmtpd
is None, 'Layer already set up'
392 cls
.lmtpd
= TestableMaster(cls
._wait
_for
_lmtp
_server
)
393 cls
.lmtpd
.start('lmtp')
397 assert cls
.lmtpd
is not None, 'Layer not set up'
403 # Make sure we don't call our superclass's testSetUp(), otherwise the
404 # example.com domain will get added twice.
409 class RESTLayer(SMTPLayer
):
410 """Layer for starting, stopping, and accessing the test REST layer."""
417 assert cls
.server
is None, 'Layer already set up'
418 cls
.server
= TestableMaster(wait_for_webservice
)
419 cls
.server
.start('rest')
423 assert cls
.server
is not None, 'Layer not set up'
430 """Return a 'testing' flag for use with the predictable factories.
432 :return: True when in testing mode.
435 return (MockAndMonkeyLayer
.testing_mode
or
436 as_boolean(config
.devmode
.testing
))