summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/database/base.py32
-rw-r--r--src/mailman/database/postgresql.py32
-rw-r--r--src/mailman/database/schema/mm_20120407000000.py86
-rw-r--r--src/mailman/database/schema/postgres.sql3
-rw-r--r--src/mailman/database/sqlite.py24
-rw-r--r--src/mailman/database/tests/data/migration_test_1.sql135
-rw-r--r--src/mailman/database/tests/test_migrations.py269
-rw-r--r--src/mailman/interfaces/database.py13
-rw-r--r--src/mailman/testing/testing.cfg6
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