diff options
| -rw-r--r-- | src/mailman/database/base.py | 32 | ||||
| -rw-r--r-- | src/mailman/database/postgresql.py | 32 | ||||
| -rw-r--r-- | src/mailman/database/schema/mm_20120407000000.py | 86 | ||||
| -rw-r--r-- | src/mailman/database/schema/postgres.sql | 3 | ||||
| -rw-r--r-- | src/mailman/database/sqlite.py | 24 | ||||
| -rw-r--r-- | src/mailman/database/tests/data/migration_test_1.sql | 135 | ||||
| -rw-r--r-- | src/mailman/database/tests/test_migrations.py | 269 | ||||
| -rw-r--r-- | src/mailman/interfaces/database.py | 13 | ||||
| -rw-r--r-- | src/mailman/testing/testing.cfg | 6 |
9 files changed, 496 insertions, 104 deletions
diff --git a/src/mailman/database/base.py b/src/mailman/database/base.py index 80c62658e..f674c9a16 100644 --- a/src/mailman/database/base.py +++ b/src/mailman/database/base.py @@ -186,6 +186,22 @@ class StormBaseDatabase: continue upgrade(self, self.store, version, module_path) + def load_sql(self, store, sql): + """Load the given SQL into the store. + + :param store: The Storm store to load the schema into. + :type store: storm.locals.Store` + :param sql: The possibly multi-line SQL to load. + :type sql: string + """ + # Discard all blank and comment lines. + lines = (line for line in sql.splitlines() + if line.strip() != '' and line.strip()[:2] != '--') + sql = NL.join(lines) + for statement in sql.split(';'): + if statement.strip() != '': + store.execute(statement + ';') + def load_schema(self, store, version, filename, module_path): """Load the schema from a file. @@ -206,17 +222,11 @@ class StormBaseDatabase: """ if filename is not None: contents = resource_string('mailman.database.schema', filename) - # Discard all blank and comment lines. - lines = (line for line in contents.splitlines() - if line.strip() != '' and line.strip()[:2] != '--') - sql = NL.join(lines) - for statement in sql.split(';'): - if statement.strip() != '': - store.execute(statement + ';') + self.load_sql(store, contents) # Add a marker that indicates the migration version being applied. store.add(Version(component='schema', version=version)) - # Add a marker so that the module name can be found later. This is - # used by the test suite to reset the database between tests. + # Add a marker so that the module name can be found later. This + # is used by the test suite to reset the database between tests. store.add(Version(component=version, version=module_path)) def _reset(self): @@ -225,3 +235,7 @@ class StormBaseDatabase: self.store.rollback() ModelMeta._reset(self.store) self.store.commit() + + @staticmethod + def _make_temporary(): + raise NotImplementedError diff --git a/src/mailman/database/postgresql.py b/src/mailman/database/postgresql.py index 988f7a1af..14855c3f3 100644 --- a/src/mailman/database/postgresql.py +++ b/src/mailman/database/postgresql.py @@ -26,11 +26,24 @@ __all__ = [ from operator import attrgetter +from urlparse import urlsplit, urlunsplit +from mailman.config import config from mailman.database.base import StormBaseDatabase + +class _TemporaryDB: + def __init__(self, database): + self.database = database + + def cleanup(self): + self.database.execute('ROLLBACK TO SAVEPOINT testing;') + self.database.execute('DROP DATABASE mmtest;') + + + class PostgreSQLDatabase(StormBaseDatabase): """Database class for PostgreSQL.""" @@ -40,8 +53,8 @@ class PostgreSQLDatabase(StormBaseDatabase): """See `BaseDatabase`.""" table_query = ('SELECT table_name FROM information_schema.tables ' "WHERE table_schema = 'public'") - table_names = set(item[0] for item in - store.execute(table_query)) + results = store.execute(table_query) + table_names = set(item[0] for item in results) return 'version' in table_names def _post_reset(self, store): @@ -63,3 +76,18 @@ class PostgreSQLDatabase(StormBaseDatabase): max("id") IS NOT null) FROM "{0}"; """.format(model_class.__storm_table__)) + + @staticmethod + def _make_temporary(): + from mailman.testing.helpers import configuration + parts = urlsplit(config.database.url) + assert parts.scheme == 'postgres' + new_parts = list(parts) + new_parts[2] = '/mmtest' + url = urlunsplit(new_parts) + database = PostgreSQLDatabase() + database.store.execute('SAVEPOINT testing;') + database.store.execute('CREATE DATABASE mmtest;') + with configuration('database', url=url): + database.initialize() + return _TemporaryDB(database) diff --git a/src/mailman/database/schema/mm_20120407000000.py b/src/mailman/database/schema/mm_20120407000000.py index ec4fcb2ee..ad0515a5d 100644 --- a/src/mailman/database/schema/mm_20120407000000.py +++ b/src/mailman/database/schema/mm_20120407000000.py @@ -15,7 +15,22 @@ # You should have received a copy of the GNU General Public License along with # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. -"""3.0b1 -> 3.0b2 schema migrations.""" +"""3.0b1 -> 3.0b2 schema migrations. + +All column changes are in the `mailinglist` table. + +* Renames: + - news_prefix_subject_too -> nntp_prefix_subject_too + - news_moderation -> newsgroup_moderation + +* Collapsing: + - archive, archive_private -> archive_policy + +* Remove: + - nntp_host + +See https://bugs.launchpad.net/mailman/+bug/971013 for details. +""" from __future__ import absolute_import, print_function, unicode_literals @@ -28,7 +43,6 @@ __all__ = [ from mailman.interfaces.archiver import ArchivePolicy -from mailman.interfaces.database import DatabaseError VERSION = '20120407000000' @@ -40,9 +54,18 @@ def upgrade(database, store, version, module_path): if database.TAG == 'sqlite': upgrade_sqlite(database, store, version, module_path) else: - # XXX 2012-04-07 BAW: Implement PostgreSQL migration. - raise DatabaseError('Database {0} migration not support: {1}'.format( - database.TAG, version)) + upgrade_postgres(database, store, version, module_path) + + + +def archive_policy(archive, archive_private): + """Convert archive and archive_private to archive_policy.""" + if archive == 0: + return int(ArchivePolicy.never) + elif archive_private == 1: + return int(ArchivePolicy.private) + else: + return int(ArchivePolicy.public) @@ -55,26 +78,53 @@ def upgrade_sqlite(database, store, version, module_path): database.load_schema( store, version, 'sqlite_{0}_01.sql'.format(version), module_path) results = store.execute( - 'select id, news_prefix_subject_too, news_moderation, ' - 'archive, archive_private from mailinglist;') + 'SELECT id, news_prefix_subject_too, news_moderation, ' + 'archive, archive_private FROM mailinglist;') for value in results: id, news_prefix, news_moderation, archive, archive_private = value # Figure out what the new archive_policy column value should be. - if archive == 0: - archive_policy = int(ArchivePolicy.never) - elif archive_private == 1: - archive_policy = int(ArchivePolicy.private) - else: - archive_policy = int(ArchivePolicy.public) store.execute( - 'update ml_backup set ' + 'UPDATE ml_backup SET ' ' newsgroup_moderation = {0}, ' ' nntp_prefix_subject_too = {1}, ' ' archive_policy = {2} ' - 'where id = {2};'.format(news_moderation, news_prefix, - archive_policy, id)) - store.execute('drop table mailinglist;') - store.execute('alter table ml_backup rename to mailinglist;') + 'WHERE id = {3};'.format(news_moderation, + news_prefix, + archive_policy(archive, archive_private), + id)) + store.execute('DROP TABLE mailinglist;') + store.execute('ALTER TABLE ml_backup RENAME TO mailinglist;') + + + +def upgrade_postgres(database, store, version, module_path): + # Get the old values from the mailinglist table. + results = store.execute( + 'SELECT id, archive, archive_private FROM mailinglist;') + # Do the simple renames first. + store.execute( + 'ALTER TABLE mailinglist ' + ' RENAME COLUMN news_prefix_subject_too TO nntp_prefix_subject_too;') + store.execute( + 'ALTER TABLE mailinglist ' + ' RENAME COLUMN news_moderation TO newsgroup_moderation;') + # Do the column drop next. + store.execute('ALTER TABLE mailinglist DROP COLUMN nntp_host;') + # Now do the trickier collapsing of values. Add the new columns. + store.execute('ALTER TABLE mailinglist ADD COLUMN archive_policy INTEGER;') + # Query the database for the old values of archive and archive_private in + # each column. Then loop through all the results and update the new + # archive_policy from the old values. + for value in results: + id, archive, archive_private = value + store.execute('UPDATE mailinglist SET ' + ' archive_policy = {0} ' + 'WHERE id = {1};'.format( + archive_policy(archive, archive_private), + id)) + # Now drop the old columns. + store.execute('ALTER TABLE mailinglist DROP COLUMN archive;') + store.execute('ALTER TABLE mailinglist DROP COLUMN archive_private;') diff --git a/src/mailman/database/schema/postgres.sql b/src/mailman/database/schema/postgres.sql index 2e9ba249f..0e97a4332 100644 --- a/src/mailman/database/schema/postgres.sql +++ b/src/mailman/database/schema/postgres.sql @@ -110,7 +110,8 @@ CREATE TABLE mailinglist ( topics_enabled BOOLEAN, unsubscribe_policy INTEGER, welcome_message_uri TEXT, - moderation_callback TEXT, + -- This was accidentally added by the PostgreSQL porter. + -- moderation_callback TEXT, PRIMARY KEY (id) ); diff --git a/src/mailman/database/sqlite.py b/src/mailman/database/sqlite.py index 2677d0d71..4e5df55a5 100644 --- a/src/mailman/database/sqlite.py +++ b/src/mailman/database/sqlite.py @@ -26,6 +26,8 @@ __all__ = [ import os +import shutil +import tempfile from urlparse import urlparse @@ -33,6 +35,16 @@ from mailman.database.base import StormBaseDatabase +class _TemporaryDB: + def __init__(self, database, tempdir): + self.database = database + self._tempdir = tempdir + + def cleanup(self): + shutil.rmtree(self._tempdir) + + + class SQLiteDatabase(StormBaseDatabase): """Database class for SQLite.""" @@ -41,7 +53,7 @@ class SQLiteDatabase(StormBaseDatabase): def _database_exists(self, store): """See `BaseDatabase`.""" table_query = 'select tbl_name from sqlite_master;' - table_names = set(item[0] for item in + table_names = set(item[0] for item in store.execute(table_query)) return 'version' in table_names @@ -54,3 +66,13 @@ class SQLiteDatabase(StormBaseDatabase): # Ignore errors if fd > 0: os.close(fd) + + @staticmethod + def _make_temporary(): + from mailman.testing.helpers import configuration + tempdir = tempfile.mkdtemp() + url = 'sqlite:///' + os.path.join(tempdir, 'mailman.db') + database = SQLiteDatabase() + with configuration('database', url=url): + database.initialize() + return _TemporaryDB(database, tempdir) diff --git a/src/mailman/database/tests/data/migration_test_1.sql b/src/mailman/database/tests/data/migration_test_1.sql new file mode 100644 index 000000000..101feea77 --- /dev/null +++ b/src/mailman/database/tests/data/migration_test_1.sql @@ -0,0 +1,135 @@ +INSERT INTO "acceptablealias" VALUES(1,'foo@example.com',1); +INSERT INTO "acceptablealias" VALUES(2,'bar@example.com',1); + +INSERT INTO "address" VALUES( + 1,'anne@example.com',NULL,'Anne Person', + '2012-04-19 00:52:24.826432','2012-04-19 00:49:42.373769',1,2); +INSERT INTO "address" VALUES( + 2,'bart@example.com',NULL,'Bart Person', + '2012-04-19 00:53:25.878800','2012-04-19 00:49:52.882050',2,4); + +-- ConfigLayer.testSetUp() will already initialize the domain. +-- +-- INSERT INTO "domain" VALUES( +-- 1,'example.com','http://example.com',NULL,'postmaster@example.com'); + +INSERT INTO "mailinglist" VALUES( + -- id,list_name,mail_host,include_list_post_header,include_rfc2369_headers + 1,'test','example.com',1,1, + -- created_at,admin_member_chunksize,next_request_id,next_digest_number + '2012-04-19 00:46:13.173844',30,1,1, + -- digest_last_sent_at,volume,last_post_at,accept_these_nonmembers + NULL,1,NULL,X'80025D71012E', + -- acceptable_aliases_id,admin_immed_notify,admin_notify_mchanges + NULL,1,0, + -- administrivia,advertised,anonymous_list,archive,archive_private + 1,1,0,1,0, + -- archive_volume_frequency + 1, + --autorespond_owner,autoresponse_owner_text + 0,'', + -- autorespond_postings,autoresponse_postings_text + 0,'', + -- autorespond_requests,authoresponse_requests_text + 0,'', + -- autoresponse_grace_period + '90 days, 0:00:00', + -- forward_unrecognized_bounces_to,process_bounces + 1,1, + -- bounce_info_stale_after,bounce_matching_headers + '7 days, 0:00:00',' +# Lines that *start* with a ''#'' are comments. +to: friend@public.com +message-id: relay.comanche.denmark.eu +from: list@listme.com +from: .*@uplinkpro.com +', + -- bounce_notify_owner_on_disable,bounce_notify_owner_on_removal + 1,1, + -- bounce_score_threshold,bounce_you_are_disabled_warnings + 5,3, + -- bounce_you_are_disabled_warnings_interval + '7 days, 0:00:00', + -- filter_action,filter_content,collapse_alternatives + 2,0,1, + -- convert_html_to_plaintext,default_member_action,default_nonmember_action + 0,4,0, + -- description + '', + -- digest_footer_uri + 'mailman:///$listname/$language/footer-generic.txt', + -- digest_header_uri + NULL, + -- digest_is_default,digest_send_periodic,digest_size_threshold + 0,1,30.0, + -- digest_volume_frequency,digestable,discard_these_nonmembers + 1,1,X'80025D71012E', + -- emergency,encode_ascii_prefixes,first_strip_reply_to + 0,0,0, + -- footer_uri + 'mailman:///$listname/$language/footer-generic.txt', + -- forward_auto_discards,gateway_to_mail,gateway_to_news + 1,0,0, + -- generic_nonmember_action,goodby_message_uri + 1,'', + -- header_matches,header_uri,hold_these_nonmembers,info,linked_newsgroup + X'80025D71012E',NULL,X'80025D71012E','','', + -- max_days_to_hold,max_message_size,max_num_recipients + 0,40,10, + -- member_moderation_notice,mime_is_default_digest,moderator_password + '',0,NULL, + -- new_member_options,news_moderation,news_prefix_subject_too + 256,0,1, + -- nntp_host,nondigestable,nonmember_rejection_notice,obscure_addresses + '',1,'',1, + -- owner_chain,owner_pipeline,personalize,post_id + 'default-owner-chain','default-owner-pipeline',0,1, + -- posting_chain,posting_pipeline,preferred_language,private_roster + 'default-posting-chain','default-posting-pipeline','en',1, + -- display_name,reject_these_nonmembers + 'Test',X'80025D71012E', + -- reply_goes_to_list,reply_to_address + 0,'', + -- require_explicit_destination,respond_to_post_requests + 1,1, + -- scrub_nondigest,send_goodbye_message,send_reminders,send_welcome_message + 0,1,1,1, + -- subject_prefix,subscribe_auto_approval + '[Test] ',X'80025D71012E', + -- subscribe_policy,topics,topics_bodylines_limit,topics_enabled + 1,X'80025D71012E',5,0, + -- unsubscribe_policy,welcome_message_uri + 0,'mailman:///welcome.txt'); + +INSERT INTO "member" VALUES( + 1,'d1243f4d-e604-4f6b-af52-98d0a7bce0f1',1,'test@example.com',4,NULL,5,1); +INSERT INTO "member" VALUES( + 2,'dccc3851-fdfb-4afa-90cf-bdcbf80ad0fd',2,'test@example.com',3,NULL,6,1); +INSERT INTO "member" VALUES( + 3,'479be431-45f2-473d-bc3c-7eac614030ac',3,'test@example.com',3,NULL,7,2); +INSERT INTO "member" VALUES( + 4,'e2dc604c-d93a-4b91-b5a8-749e3caade36',1,'test@example.com',4,NULL,8,2); + +INSERT INTO "preferences" VALUES(1,NULL,NULL,NULL,NULL,NULL,NULL,NULL); +INSERT INTO "preferences" VALUES(2,NULL,NULL,NULL,NULL,NULL,NULL,NULL); +INSERT INTO "preferences" VALUES(3,NULL,NULL,NULL,NULL,NULL,NULL,NULL); +INSERT INTO "preferences" VALUES(4,NULL,NULL,NULL,NULL,NULL,NULL,NULL); +INSERT INTO "preferences" VALUES(5,NULL,NULL,NULL,NULL,NULL,NULL,NULL); +INSERT INTO "preferences" VALUES(6,NULL,NULL,NULL,NULL,NULL,NULL,NULL); +INSERT INTO "preferences" VALUES(7,NULL,NULL,NULL,NULL,NULL,NULL,NULL); +INSERT INTO "preferences" VALUES(8,NULL,NULL,NULL,NULL,NULL,NULL,NULL); + +INSERT INTO "user" VALUES( + 1,'Anne Person',NULL,'0adf3caa-6f26-46f8-a11d-5256c8148592', + '2012-04-19 00:49:42.370493',1,1); +INSERT INTO "user" VALUES( + 2,'Bart Person',NULL,'63f5d1a2-e533-4055-afe4-475dec3b1163', + '2012-04-19 00:49:52.868746',2,3); + +INSERT INTO "uid" VALUES(1,'8bf9a615-f23e-4980-b7d1-90ac0203c66f'); +INSERT INTO "uid" VALUES(2,'0adf3caa-6f26-46f8-a11d-5256c8148592'); +INSERT INTO "uid" VALUES(3,'63f5d1a2-e533-4055-afe4-475dec3b1163'); +INSERT INTO "uid" VALUES(4,'d1243f4d-e604-4f6b-af52-98d0a7bce0f1'); +INSERT INTO "uid" VALUES(5,'dccc3851-fdfb-4afa-90cf-bdcbf80ad0fd'); +INSERT INTO "uid" VALUES(6,'479be431-45f2-473d-bc3c-7eac614030ac'); +INSERT INTO "uid" VALUES(7,'e2dc604c-d93a-4b91-b5a8-749e3caade36'); diff --git a/src/mailman/database/tests/test_migrations.py b/src/mailman/database/tests/test_migrations.py index c772a63d5..0e6edae47 100644 --- a/src/mailman/database/tests/test_migrations.py +++ b/src/mailman/database/tests/test_migrations.py @@ -21,7 +21,9 @@ from __future__ import absolute_import, print_function, unicode_literals __metaclass__ = type __all__ = [ - 'TestMigration20120407', + 'TestMigration20120407ArchiveData', + 'TestMigration20120407Data', + 'TestMigration20120407Schema', ] @@ -31,11 +33,12 @@ import sqlite3 import tempfile import unittest -from pkg_resources import resource_filename +from pkg_resources import resource_string from zope.component import getUtility from mailman.config import config from mailman.interfaces.domain import IDomainManager +from mailman.interfaces.archiver import ArchivePolicy from mailman.interfaces.listmanager import IListManager from mailman.interfaces.mailinglist import IAcceptableAliasSet from mailman.testing.helpers import configuration, temporary_db @@ -44,7 +47,7 @@ from mailman.utilities.modules import call_name -class TestMigration20120407(unittest.TestCase): +class TestMigration20120407Schema(unittest.TestCase): """Test the dated migration (LP: #971013) Circa: 3.0b1 -> 3.0b2 @@ -62,26 +65,25 @@ class TestMigration20120407(unittest.TestCase): layer = ConfigLayer def setUp(self): - self._tempdir = tempfile.mkdtemp() + database_class_name = config.database['class'] + database_class = call_name(database_class_name) + self._temporary = database_class._make_temporary() + self._database = self._temporary.database def tearDown(self): - shutil.rmtree(self._tempdir) + self._temporary.cleanup() def test_sqlite_base(self): # Test that before the migration, the old table columns are present # and the new database columns are not. - url = 'sqlite:///' + os.path.join(self._tempdir, 'mailman.db') - database_class = config.database['class'] - database = call_name(database_class) - with configuration('database', url=url): - database.initialize() - # Load all the database SQL to just before ours. - database.load_migrations('20120406999999') + # + # Load all the migrations to just before the one we're testing. + self._database.load_migrations('20120406999999') # Verify that the database has not yet been migrated. for missing in ('archive_policy', 'nntp_prefix_subject_too'): self.assertRaises(sqlite3.OperationalError, - database.store.execute, + self._database.store.execute, 'select {0} from mailinglist;'.format(missing)) for present in ('archive', 'archive_private', @@ -91,27 +93,22 @@ class TestMigration20120407(unittest.TestCase): 'nntp_host'): # This should not produce an exception. Is there some better test # that we can perform? - database.store.execute( + self._database.store.execute( 'select {0} from mailinglist;'.format(present)) def test_sqlite_migration(self): # Test that after the migration, the old table columns are missing # and the new database columns are present. - url = 'sqlite:///' + os.path.join(self._tempdir, 'mailman.db') - database_class = config.database['class'] - database = call_name(database_class) - with configuration('database', url=url): - database.initialize() - # Load all the database SQL to just before ours. - database.load_migrations('20120406999999') - # Load all migrations, up to and including this one. - database.load_migrations('20120407000000') + # + # Load all the migrations up to and including the one we're testing. + self._database.load_migrations('20120406999999') + self._database.load_migrations('20120407000000') # Verify that the database has been migrated. for present in ('archive_policy', 'nntp_prefix_subject_too'): # This should not produce an exception. Is there some better test # that we can perform? - database.store.execute( + self._database.store.execute( 'select {0} from mailinglist;'.format(present)) for missing in ('archive', 'archive_private', @@ -120,51 +117,183 @@ class TestMigration20120407(unittest.TestCase): 'news_prefix_subject_too', 'nntp_host'): self.assertRaises(sqlite3.OperationalError, - database.store.execute, + self._database.store.execute, 'select {0} from mailinglist;'.format(missing)) - def test_data_after_migration(self): - # Ensure that the existing data and foreign key references are - # preserved across a migration. Unfortunately, this requires sample - # data, which kind of sucks. - dst = os.path.join(self._tempdir, 'mailman.db') - src = resource_filename('mailman.database.tests.data', 'mailman_01.db') - shutil.copyfile(src, dst) - url = 'sqlite:///' + dst - database_class = config.database['class'] - database = call_name(database_class) - with configuration('database', url=url): - # Initialize the database and perform the migrations. - database.initialize() - database.load_migrations('20120407000000') - with temporary_db(database): - # Check that the domains survived the migration. This table - # was not touched so it should be fine. - domains = list(getUtility(IDomainManager)) - self.assertEqual(len(domains), 1) - self.assertEqual(domains[0].mail_host, 'example.com') - # There should be exactly one mailing list defined. - mlists = list(getUtility(IListManager).mailing_lists) - self.assertEqual(len(mlists), 1) - # Get the mailing list object and check its acceptable - # aliases. This tests that foreign keys continue to work. - mlist = mlists[0] - aliases_set = IAcceptableAliasSet(mlist) - self.assertEqual(set(aliases_set.aliases), - set(['foo@example.com', 'bar@example.com'])) - # Test that all the members we expect are still there. Start - # with the two list delivery members. - addresses = set(address.email - for address in mlist.members.addresses) - self.assertEqual(addresses, set(['anne@example.com', - 'bart@example.com'])) - # There is one owner. - owners = set(address.email - for address in mlist.owners.addresses) - self.assertEqual(len(owners), 1) - self.assertEqual(owners.pop(), 'anne@example.com') - # There is one moderator. - moderators = set(address.email - for address in mlist.moderators.addresses) - self.assertEqual(len(moderators), 1) - self.assertEqual(moderators.pop(), 'bart@example.com') + + +class TestMigration20120407Data(unittest.TestCase): + """Test the dated migration (LP: #971013) + + Circa: 3.0b1 -> 3.0b2 + + table mailinglist: + * news_moderation -> newsgroup_moderation + * news_prefix_subject_too -> nntp_prefix_subject_too + * ADD archive_policy + * REMOVE archive + * REMOVE archive_private + * REMOVE archive_volume_frequency + * REMOVE nntp_host + """ + + layer = ConfigLayer + + def setUp(self): + database_class_name = config.database['class'] + database_class = call_name(database_class_name) + self._temporary = database_class._make_temporary() + self._database = self._temporary.database + # Load all the migrations to just before the one we're testing. + self._database.load_migrations('20120406999999') + # Load the previous schema's sample data. + sample_data = resource_string( + 'mailman.database.tests.data', 'migration_test_1.sql') + self._database.load_sql(self._database.store, sample_data) + # Update to the current migration we're testing. + self._database.load_migrations('20120407000000') + + def tearDown(self): + self._temporary.cleanup() + + def test_migration_domains(self): + # Test that the domains table, which isn't touched, doesn't change. + with temporary_db(self._database): + # Check that the domains survived the migration. This table + # was not touched so it should be fine. + domains = list(getUtility(IDomainManager)) + self.assertEqual(len(domains), 1) + self.assertEqual(domains[0].mail_host, 'example.com') + + def test_migration_mailing_lists(self): + # Test that the mailing lists survive migration. + with temporary_db(self._database): + # There should be exactly one mailing list defined. + mlists = list(getUtility(IListManager).mailing_lists) + self.assertEqual(len(mlists), 1) + self.assertEqual(mlists[0].fqdn_listname, 'test@example.com') + + def test_migration_acceptable_aliases(self): + # Test that the mailing list's acceptable aliases survive migration. + # This proves that foreign key references are migrated properly. + with temporary_db(self._database): + mlist = getUtility(IListManager).get('test@example.com') + aliases_set = IAcceptableAliasSet(mlist) + self.assertEqual(set(aliases_set.aliases), + set(['foo@example.com', 'bar@example.com'])) + + def test_migration_members(self): + # Test that the members of a mailing list all survive migration. + with temporary_db(self._database): + mlist = getUtility(IListManager).get('test@example.com') + # Test that all the members we expect are still there. Start with + # the two list delivery members. + addresses = set(address.email + for address in mlist.members.addresses) + self.assertEqual(addresses, + set(['anne@example.com', 'bart@example.com'])) + # There is one owner. + owners = set(address.email for address in mlist.owners.addresses) + self.assertEqual(len(owners), 1) + self.assertEqual(owners.pop(), 'anne@example.com') + # There is one moderator. + moderators = set(address.email + for address in mlist.moderators.addresses) + self.assertEqual(len(moderators), 1) + self.assertEqual(moderators.pop(), 'bart@example.com') + + + +class TestMigration20120407ArchiveData(unittest.TestCase): + """Test the dated migration (LP: #971013) + + Circa: 3.0b1 -> 3.0b2 + + table mailinglist: + * news_moderation -> newsgroup_moderation + * news_prefix_subject_too -> nntp_prefix_subject_too + * ADD archive_policy + * REMOVE archive + * REMOVE archive_private + * REMOVE archive_volume_frequency + * REMOVE nntp_host + """ + + layer = ConfigLayer + + def setUp(self): + database_class_name = config.database['class'] + database_class = call_name(database_class_name) + self._temporary = database_class._make_temporary() + self._database = self._temporary.database + # Load all the migrations to just before the one we're testing. + self._database.load_migrations('20120406999999') + # Load the previous schema's sample data. + sample_data = resource_string( + 'mailman.database.tests.data', 'migration_test_1.sql') + self._database.load_sql(self._database.store, sample_data) + + def _upgrade(self): + # Update to the current migration we're testing. + self._database.load_migrations('20120407000000') + + def tearDown(self): + self._temporary.cleanup() + + def test_migration_archive_policy_never_0(self): + # Test that the new archive_policy value is updated correctly. In the + # case of old column archive=0, the archive_private column is + # ignored. This test sets it to 0 to ensure it's ignored. + with configuration('database', url=self._url): + # Set the old archive values. + self._database.store.execute( + 'UPDATE mailinglist SET archive = 0, archive_private = 0 ' + 'WHERE id = 1;') + # Complete the migration + self._upgrade() + with temporary_db(self._database): + mlist = getUtility(IListManager).get('test@example.com') + self.assertEqual(mlist.archive_policy, ArchivePolicy.never) + + def test_migration_archive_policy_never_1(self): + # Test that the new archive_policy value is updated correctly. In the + # case of old column archive=0, the archive_private column is + # ignored. This test sets it to 1 to ensure it's ignored. + with configuration('database', url=self._url): + # Set the old archive values. + self._database.store.execute( + 'UPDATE mailinglist SET archive = 0, archive_private = 1 ' + 'WHERE id = 1;') + # Complete the migration + self._upgrade() + with temporary_db(self._database): + mlist = getUtility(IListManager).get('test@example.com') + self.assertEqual(mlist.archive_policy, ArchivePolicy.never) + + def test_archive_policy_private(self): + # Test that the new archive_policy value is updated correctly for + # private archives. + with configuration('database', url=self._url): + # Set the old archive values. + self._database.store.execute( + 'UPDATE mailinglist SET archive = 1, archive_private = 1 ' + 'WHERE id = 1;') + # Complete the migration + self._upgrade() + with temporary_db(self._database): + mlist = getUtility(IListManager).get('test@example.com') + self.assertEqual(mlist.archive_policy, ArchivePolicy.private) + + def test_archive_policy_public(self): + # Test that the new archive_policy value is updated correctly for + # public archives. + with configuration('database', url=self._url): + # Set the old archive values. + self._database.store.execute( + 'UPDATE mailinglist SET archive = 1, archive_private = 0 ' + 'WHERE id = 1;') + # Complete the migration + self._upgrade() + with temporary_db(self._database): + mlist = getUtility(IListManager).get('test@example.com') + self.assertEqual(mlist.archive_policy, ArchivePolicy.public) diff --git a/src/mailman/interfaces/database.py b/src/mailman/interfaces/database.py index 040bce77c..316d5be49 100644 --- a/src/mailman/interfaces/database.py +++ b/src/mailman/interfaces/database.py @@ -55,6 +55,19 @@ class IDatabase(Interface): This is only used by the test framework. """ + def _make_temporary(): + """Make a temporary database. + + This is a @staticmethod used in the test framework. + + :return: An object with one attribute and one method. The attribute + `database` is the temporary `IDatabase`. The method is a callable + named `cleanup()`, taking no arguments which should be called when + the temporary database is no longer necessary. The database will + already be initialized, but no migrations will have been loaded + into it. + """ + def begin(): """Begin the current transaction.""" diff --git a/src/mailman/testing/testing.cfg b/src/mailman/testing/testing.cfg index 5f19dca14..0be01298b 100644 --- a/src/mailman/testing/testing.cfg +++ b/src/mailman/testing/testing.cfg @@ -18,9 +18,9 @@ # A testing configuration. # For testing against PostgreSQL. -#[database] -#class: mailman.database.postgresql.PostgreSQLDatabase -#url: postgres://barry:barry@localhost/mailman +[database] +class: mailman.database.postgresql.PostgreSQLDatabase +url: postgres://barry:barry@localhost/mailman [mailman] site_owner: noreply@example.com |
