def test_no_action_defaults_to_site_wide_action(self): # If the list-specific header check matches, but there is no defined # action, the site-wide antispam action is used. msg = mfs("""\ From: [email protected] To: [email protected] Subject: A message Message-ID: <ant> Foo: foo MIME-Version: 1.0 A message body. """) header_matches = IHeaderMatchList(self._mlist) header_matches.append('Foo', 'foo') # This event subscriber records the event that occurs when the message # is processed by the owner chain, which holds its for approval. events = [] def record_holds(event): # noqa: E301 if not isinstance(event, HoldEvent): return events.append(event) with event_subscribers(record_holds): # Set the site-wide antispam action to hold the message. with configuration('antispam', header_checks=""" Spam: [*]{3,} """, jump_chain='hold'): # noqa: E125 process(self._mlist, msg, {}, start_chain='header-match') self.assertEqual(len(events), 1) event = events[0] self.assertIsInstance(event, HoldEvent) self.assertEqual(event.chain, config.chains['hold']) self.assertEqual(event.mlist, self._mlist) self.assertEqual(event.msg, msg) events = [] def record_discards(event): # noqa: E301 if not isinstance(event, DiscardEvent): return events.append(event) with event_subscribers(record_discards): # Set the site-wide default to discard the message. msg.replace_header('Message-Id', '<bee>') with configuration('antispam', header_checks=""" Spam: [*]{3,} """, jump_chain='discard'): # noqa: E125 process(self._mlist, msg, {}, start_chain='header-match') self.assertEqual(len(events), 1) event = events[0] self.assertIsInstance(event, DiscardEvent) self.assertEqual(event.chain, config.chains['discard']) self.assertEqual(event.mlist, self._mlist) self.assertEqual(event.msg, msg)
def test_no_action_defaults_to_site_wide_action(self): # If the list-specific header check matches, but there is no defined # action, the site-wide antispam action is used. msg = mfs("""\ From: [email protected] To: [email protected] Subject: A message Message-ID: <ant> Foo: foo MIME-Version: 1.0 A message body. """) header_matches = IHeaderMatchList(self._mlist) header_matches.append('Foo', 'foo') # This event subscriber records the event that occurs when the message # is processed by the owner chain, which holds its for approval. events = [] def record_holds(event): # noqa if not isinstance(event, HoldEvent): return events.append(event) with event_subscribers(record_holds): # Set the site-wide antispam action to hold the message. with configuration('antispam', header_checks=""" Spam: [*]{3,} """, jump_chain='hold'): # noqa process(self._mlist, msg, {}, start_chain='header-match') self.assertEqual(len(events), 1) event = events[0] self.assertIsInstance(event, HoldEvent) self.assertEqual(event.chain, config.chains['hold']) self.assertEqual(event.mlist, self._mlist) self.assertEqual(event.msg, msg) events = [] def record_discards(event): # noqa if not isinstance(event, DiscardEvent): return events.append(event) with event_subscribers(record_discards): # Set the site-wide default to discard the message. msg.replace_header('Message-Id', '<bee>') with configuration('antispam', header_checks=""" Spam: [*]{3,} """, jump_chain='discard'): # noqa process(self._mlist, msg, {}, start_chain='header-match') self.assertEqual(len(events), 1) event = events[0] self.assertIsInstance(event, DiscardEvent) self.assertEqual(event.chain, config.chains['discard']) self.assertEqual(event.mlist, self._mlist) self.assertEqual(event.msg, msg)
def test_push_and_pop_trigger_events(self): # Pushing a new configuration onto the stack triggers a # post-processing event. events = [] def on_event(event): # noqa: E306 if isinstance(event, ConfigurationUpdatedEvent): # Record both the event and the top overlay. events.append(event.config.overlays[0].name) # Do two pushes, and then pop one of them. with event_subscribers(on_event): with configuration('test', _configname='first'): with configuration('test', _configname='second'): pass self.assertEqual(events, ['first', 'second', 'first'])
def test_push_and_pop_trigger_events(self): # Pushing a new configuration onto the stack triggers a # post-processing event. events = [] def on_event(event): if isinstance(event, ConfigurationUpdatedEvent): # Record both the event and the top overlay. events.append(event.config.overlays[0].name) # Do two pushes, and then pop one of them. with event_subscribers(on_event): with configuration('test', _configname='first'): with configuration('test', _configname='second'): pass self.assertEqual(events, ['first', 'second', 'first'])
def test_successful_login_updates_password(self): # Passlib supports updating the hash when the hash algorithm changes. # When a user logs in successfully, the password will be updated if # necessary. # # Start by hashing Anne's password with a different hashing algorithm # than the one that the REST runner uses by default during testing. config_file = os.path.join(config.VAR_DIR, 'passlib-tmp.config') with open(config_file, 'w') as fp: print("""\ [passlib] schemes = hex_md5 """, file=fp) with configuration('passwords', configuration=config_file): with transaction(): self.anne.password = config.password_context.encrypt('abc123') # Just ensure Anne's password is hashed correctly. self.assertEqual(self.anne.password, 'e99a18c428cb38d5f260853678922e03') # Now, Anne logs in with a successful password. This should change it # back to the plaintext hash. call_api('http://localhost:9001/3.0/users/1/login', { 'cleartext_password': '******', }) self.assertEqual(self.anne.password, '{plaintext}abc123')
def test_verp_never(self): # Never VERP when the interval is zero. msgdata = {} self._outq.enqueue(self._msg, msgdata, listid='test.example.com') with configuration('mta', verp_delivery_interval=0): self._runner.run() self.assertFalse(captured_msgdata['verp'])
def test_verp_always(self): # Always VERP when the interval is one. msgdata = {} self._outq.enqueue(self._msg, msgdata, listid='test.example.com') with configuration('mta', verp_delivery_interval=1): self._runner.run() self.assertTrue(captured_msgdata['verp'])
def use_test_organizational_data(resources): # Point the organizational URL to our test data. filename = str(resources.enter_context( path('mailman.rules.tests.data', 'org_domain.txt'))) url = 'file:///{}'.format(filename) return resources.enter_context( configuration('dmarc', org_domain_data_url=url))
def test_no_verp_on_interval_miss(self): # VERP every so often, when the post_id matches. self._mlist.post_id = 4 msgdata = {} self._outq.enqueue(self._msg, msgdata, listid='test.example.com') with configuration('mta', verp_delivery_interval=5): self._runner.run() self.assertFalse(captured_msgdata['verp'])
def test_personalized_full_deliveries_verp(self): # When deliveries are personalized, and the configuration setting # indicates, messages will be VERP'd. msgdata = {} self._mlist.personalize = Personalization.full self._outq.enqueue(self._msg, msgdata, listid='test.example.com') with configuration('mta', verp_personalized_deliveries='yes'): self._runner.run() self.assertTrue(captured_msgdata['verp'])
def test_mhonarc(self): # The archiver properly sends stdin to the subprocess. with configuration('archiver.mhonarc', configuration=self._cfg, enable='yes'): MHonArc().archive_message(self._mlist, self._msg) with open(self._output_file, 'r', encoding='utf-8') as fp: results = fp.read().splitlines() self.assertEqual(results[0], '<ant>') self.assertEqual(results[1], 'MS6QLWERIJLGCRF44J7USBFDELMNT2BW')
def test_find_pluggable_components_by_plugin_name(self): path = resource_filename('mailman.plugins.testing', '') with ExitStack() as resources: resources.enter_context(hack_syspath(0, path)) resources.enter_context( configuration( 'plugin.example', **{ 'class': 'example.hooks.ExamplePlugin', 'enabled': 'yes', })) components = list(find_pluggable_components('rules', IRule)) self.assertIn('example-rule', {rule.name for rule in components})
def make_temporary(database): """Adapts by monkey patching an existing SQLite IDatabase.""" tempdir = tempfile.mkdtemp() url = 'sqlite:///' + os.path.join(tempdir, 'mailman.db') with configuration('database', url=url): database.initialize() database._cleanup = types.MethodType( partial(_cleanup, tempdir=tempdir), database) # bool column values in SQLite must be integers. database.FALSE = 0 database.TRUE = 1 return database
def test_find_pluggable_components_by_component_package(self): with ExitStack() as resources: testing_path = resources.enter_context( path('mailman.plugins.testing', '')) resources.enter_context(hack_syspath(0, str(testing_path))) resources.enter_context( configuration( 'plugin.example', **{ 'class': 'example.hooks.ExamplePlugin', 'enabled': 'yes', 'component_package': 'alternate', })) components = list(find_pluggable_components('rules', IRule)) self.assertNotIn('example-rule', {rule.name for rule in components}) self.assertIn('alternate-rule', {rule.name for rule in components})
def test_error_with_numeric_port(self): # Test the code path where a socket.error is raised in the delivery # function, and the MTA port is set to zero. The only real effect of # that is a log message. Start by opening the error log and reading # the current file position. mark = LogFileMark('mailman.error') self._outq.enqueue(self._msg, {}, listid='test.example.com') with configuration('mta', smtp_port=2112): self._runner.run() line = mark.readline() # The log line will contain a variable timestamp, the PID, and a # trailing newline. Ignore these. self.assertEqual( line[-53:-1], 'Cannot connect to SMTP server localhost on port 2112')
def test_passlib_from_file_path(self): # Set up this test to use a passlib configuration file specified with # a file system path. We prove we're using the new configuration # because a non-prefixed, i.e. non-roundup, plaintext hash algorithm # will be used. When a file system path is used, the file can end in # any suffix. config_file = os.path.join(config.VAR_DIR, 'passlib.config') with open(config_file, 'w') as fp: print("""\ [passlib] schemes = plaintext """, file=fp) with configuration('passwords', configuration=config_file): self.assertEqual(config.password_context.encrypt('my password'), 'my password')
def test_push_and_pop_trigger_events(self): # Pushing a new configuration onto the stack triggers a # post-processing event. events = [] def on_event(event): if isinstance(event, ConfigurationUpdatedEvent): # Record both the event and the top overlay. events.append(event.config.overlays[0].name) with event_subscribers(on_event): with configuration('test', _configname='my test'): pass # There should be two pushed configuration names on the list now, one # for the push leaving 'my test' on the top of the stack, and one for # the pop, leaving the ConfigLayer's 'test config' on top. self.assertEqual(events, ['my test', 'test config'])
def test_master_is_elsewhere_and_findable(self): with ExitStack() as resources: bin_dir = resources.enter_context(TemporaryDirectory()) old_master = os.path.join(config.BIN_DIR, 'master') new_master = os.path.join(bin_dir, 'master') shutil.move(old_master, new_master) resources.enter_context( configuration('paths.testing', bin_dir=bin_dir)) resources.callback(shutil.move, new_master, old_master) # Starting mailman should find master in the new bin_dir. self.command.process(self.args) # There should a pid file and the process it describes should be # killable. We might have to wait until the process has started. master_pid = find_master() self.assertIsNotNone(master_pid, 'master did not start') kill_with_extreme_prejudice(master_pid)
def test_list_id_and_language_code_allowed_in_template_uri(self): # Issue #196 - allow the list_id in the template uri expansion. list_dir = os.path.join(config.TEMPLATE_DIR, 'lists', 'ant.example.com', 'it') os.makedirs(list_dir) footer_path = os.path.join(list_dir, 'myfooter.txt') with open(footer_path, 'w', encoding='utf-8') as fp: print('${testarchiver_url}', file=fp) getUtility(ITemplateManager).set( 'list:member:regular:footer', self._mlist.list_id, 'mailman:///${list_id}/${language}/myfooter.txt') self._mlist.preferred_language = 'it' with configuration('language.it', charset='iso-8859-1'): # Default charset='utf-8' base64 encodes the message body. decorate.process(self._mlist, self._msg, {}) self.assertIn('http://example.com/link_to_message', self._msg.as_string())
def test_master_is_elsewhere_and_findable(self): with ExitStack() as resources: bin_dir = resources.enter_context(TemporaryDirectory()) old_master = os.path.join(config.BIN_DIR, 'master') new_master = os.path.join(bin_dir, 'master') shutil.move(old_master, new_master) resources.callback(shutil.move, new_master, old_master) with configuration('paths.testing', bin_dir=bin_dir): results = self._command.invoke(start) # Argument #2 to the execl() call should be the path to the master # program, and the path should exist. self.assertEqual(len(self._execl.call_args_list), 1, results.output) posargs, kws = self._execl.call_args_list[0] master_path = posargs[2] self.assertEqual(os.path.basename(master_path), 'master') self.assertTrue(os.path.exists(master_path), master_path)
def test_master_is_elsewhere_and_findable(self): with ExitStack() as resources: bin_dir = resources.enter_context(TemporaryDirectory()) old_master = os.path.join(config.BIN_DIR, 'master') new_master = os.path.join(bin_dir, 'master') shutil.move(old_master, new_master) resources.enter_context( configuration('paths.testing', bin_dir=bin_dir)) resources.callback(shutil.move, new_master, old_master) # Starting mailman should find master in the new bin_dir. self.command.process(self.args) # There should a pid file and the process it describes should be # killable. We might have to wait until the process has started. master_pid = find_master() self.assertIsNotNone(master_pid, 'master did not start') os.kill(master_pid, signal.SIGTERM) os.waitpid(master_pid, 0)
def test_error_with_port_0(self): # Test the code path where a socket.error is raised in the delivery # function, and the MTA port is set to zero. The only real effect of # that is a log message. Start by opening the error log and reading # the current file position. error_log = logging.getLogger('mailman.error') filename = error_log.handlers[0].filename filepos = os.stat(filename).st_size self._outq.enqueue(self._msg, {}, listid='test.example.com') with configuration('mta', smtp_port=0): self._runner.run() with open(filename) as fp: fp.seek(filepos) line = fp.readline() # The log line will contain a variable timestamp, the PID, and a # trailing newline. Ignore these. self.assertEqual( line[-53:-1], 'Cannot connect to SMTP server localhost on port smtp')
def make_temporary(database): """Adapts by monkey patching an existing PostgreSQL IDatabase.""" from mailman.config import config parts = urlsplit(config.database.url) assert parts.scheme == "postgres" new_parts = list(parts) new_parts[2] = "/mmtest" url = urlunsplit(new_parts) # Use the existing database connection to create a new testing # database. config.db.store.execute("ABORT;") config.db.store.execute("CREATE DATABASE mmtest;") with configuration("database", url=url): database.initialize() database._cleanup = types.MethodType(partial(_cleanup, store=database.store, tempdb_name="mmtest"), database) # bool column values in PostgreSQL. database.FALSE = "False" database.TRUE = "True" return database
def test_valid_password_migrates(self): # Now that the moderator password is set, change the default password # hashing algorithm. When the old password is validated, it will be # automatically migrated to the new hash. self.assertEqual(self._mlist.moderator_password, '{plaintext}super secret') config_file = os.path.join(config.VAR_DIR, 'passlib.config') # XXX passlib seems to choose the default hashing scheme even if it is # deprecated. The default scheme is either specified explicitly, or # is the first in this list. This seems like a bug. with open(config_file, 'w') as fp: print("""\ [passlib] schemes = roundup_plaintext, plaintext default = plaintext deprecated = roundup_plaintext """, file=fp) with configuration('passwords', configuration=config_file): self._msg['Approved'] = 'super secret' result = self._rule.check(self._mlist, self._msg, {}) self.assertTrue(result) self.assertEqual(self._mlist.moderator_password, 'super secret')
def use_test_organizational_data(): # Point the organizational URL to our test data. path = resource_filename('mailman.rules.tests.data', 'org_domain.txt') url = 'file:///{}'.format(path) return configuration('dmarc', org_domain_data_url=url)