Merge lp:~jtv/launchpad/bug-590688 into lp:launchpad

Proposed by Jeroen T. Vermeulen on 2010-06-07
Status: Merged
Approved by: Jeroen T. Vermeulen on 2010-06-07
Approved revision: no longer in the source branch.
Merged at revision: 10957
Proposed branch: lp:~jtv/launchpad/bug-590688
Merge into: lp:launchpad
Diff against target: 249 lines (+60/-28)
4 files modified
database/schema/security.cfg (+1/-0)
lib/lp/testing/__init__.py (+12/-0)
lib/lp/translations/scripts/tests/test_translations_to_branch.py (+28/-1)
lib/lp/translations/tests/test_autoapproval.py (+19/-27)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-590688
Reviewer Review Type Date Requested Status
Henning Eggers (community) code 2010-06-07 Approve on 2010-06-07
Review via email: mp+26929@code.launchpad.net

Commit Message

Missing database privilege for translations-export-to-branch script.

Description of the Change

= Bug 590688 =

The fix for bug 409686 involves sending an email notification to the owners of any branches that are set up to have translations exported to them, but have never been created in codehosting. (This happens when a user goes through the "register a branch" process in the UI but fails to push a branch to the newly reserved location). For various reasons we have discussed elsewhere, just creating an empty branch is not the right thing to do.

In composing the notification email, the fix for bug 409686 accessed a Branch property that triggers a SELECT on the SeriesSourcePackageBranch table. The script runs under a database user that has no select privileges on that table, and so it fails with a privilege error. This branch applies the missing privilege, and extends the unit test to cover the new code path under a realistic database user identity.

I'm also adding a becomeDbUser method to TestCase, to encourage more realistic testing, and updating an older test to make use of TestCaseWithFactory instead of unittest.TestCase.

To test:
{{{
./bin/test -vv -t translations_to_branch -t test_autoapproval
}}}

To Q/A, see that translations export to branch starts working again on staging. Since none of the branches set up for translations export in the staging database normally have their code hosted on staging (the database being a copy of the production database), all runs of translations-export-to-branch.py on staging fail with this privilege error.

Jeroen

To post a comment you must log in.
Henning Eggers (henninge) wrote :

<henninge> jtv: I wonder if the method should not be called "switchDbUser" like the Layer function.
<jtv> henninge: I wanted this to be "attractive" and have a name that doesn't say "you probably don't need to know about this." At the same time, this doesn't just switch the db user but also commits because otherwise the switch doesn't make much sense. So naming it after one part of the task would be misleading.
<henninge> I had a feeling you'd say something like that ... ;)
<jtv> ah :)
<henninge> jtv: I am not sure I go a long with your assesment of "attractive" method names but it's not all that important, I guess.

review: Approve (code)
Jeroen T. Vermeulen (jtv) wrote :

Thanks. A better name would be welcome, of course.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2010-05-27 19:12:43 +0000
3+++ database/schema/security.cfg 2010-06-07 10:42:25 +0000
4@@ -1394,6 +1394,7 @@
5 public.potranslation = SELECT
6 public.product = SELECT
7 public.productseries = SELECT
8+public.seriessourcepackagebranch = SELECT
9 public.translationmessage = SELECT
10 public.translationtemplateitem = SELECT
11
12
13=== modified file 'lib/lp/testing/__init__.py'
14--- lib/lp/testing/__init__.py 2010-05-28 23:06:25 +0000
15+++ lib/lp/testing/__init__.py 2010-06-07 10:42:25 +0000
16@@ -220,6 +220,17 @@
17
18 class TestCase(testtools.TestCase):
19 """Provide Launchpad-specific test facilities."""
20+ def becomeDbUser(self, dbuser):
21+ """Commit, then log into the database as `dbuser`.
22+
23+ For this to work, the test must run in a layer.
24+
25+ Try to test every code path at least once under a realistic db
26+ user, or you'll hit privilege violations later on.
27+ """
28+ assert self.layer, "becomeDbUser requires a layer."
29+ transaction.commit()
30+ self.layer.switchDbUser(dbuser)
31
32 def installFixture(self, fixture):
33 """Install 'fixture', an object that has a `setUp` and `tearDown`.
34@@ -437,6 +448,7 @@
35 self._unfoldEmailHeader(expected),
36 self._unfoldEmailHeader(observed))
37
38+
39 class TestCaseWithFactory(TestCase):
40
41 def setUp(self, user=ANONYMOUS):
42
43=== modified file 'lib/lp/translations/scripts/tests/test_translations_to_branch.py'
44--- lib/lp/translations/scripts/tests/test_translations_to_branch.py 2010-05-27 14:20:42 +0000
45+++ lib/lp/translations/scripts/tests/test_translations_to_branch.py 2010-06-07 10:42:25 +0000
46@@ -139,12 +139,15 @@
47 def test_exportToBranches_handles_nonascii_exceptions(self):
48 # There's an exception handler in _exportToBranches that must
49 # cope well with non-ASCII exception strings.
50+ productseries = self.factory.makeProductSeries()
51 exporter = ExportTranslationsToBranch(test_args=[])
52 exporter.logger = QuietFakeLogger()
53 boom = u'\u2639'
54 exporter._exportToBranch = FakeMethod(failure=GruesomeException(boom))
55
56- exporter._exportToBranches([self.factory.makeProductSeries()])
57+ self.becomeDbUser('translationstobranch')
58+
59+ exporter._exportToBranches([productseries])
60
61 self.assertEqual(1, exporter._exportToBranch.call_count)
62
63@@ -162,6 +165,8 @@
64 productseries = self.factory.makeProductSeries()
65 productseries.translations_branch = self.factory.makeBranch()
66
67+ self.becomeDbUser('translationstobranch')
68+
69 # _handleUnpushedBranch is called if _exportToBranch raises
70 # NotBranchError.
71 exporter._handleUnpushedBranch = FakeMethod()
72@@ -192,6 +197,8 @@
73 exporter._exportToBranch = FakeMethod(failure=NotBranchError("Ow"))
74 exporter._sendMail = FakeMethod()
75
76+ self.becomeDbUser('translationstobranch')
77+
78 exporter._exportToBranches([productseries])
79
80 self.assertEqual(1, exporter._sendMail.call_count)
81@@ -208,6 +215,26 @@
82 self.assertIn(productseries.translations_branch.bzr_identity, text)
83 self.assertIn('bzr push lp://', text)
84
85+ def test_handleUnpushedBranch_has_required_privileges(self):
86+ # Dealing with an unpushed branch is a special code path that
87+ # was not exercised by the full-script test. Ensure that it has
88+ # the database privileges that it requires.
89+ exporter = ExportTranslationsToBranch(test_args=[])
90+ exporter.logger = QuietFakeLogger()
91+ productseries = self.factory.makeProductSeries()
92+ email = self.factory.getUniqueEmailAddress()
93+ branch_owner = self.factory.makePerson(email=email)
94+ productseries.translations_branch = self.factory.makeBranch(
95+ owner=branch_owner)
96+ exporter._exportToBranch = FakeMethod(failure=NotBranchError("Ow"))
97+
98+ self.becomeDbUser('translationstobranch')
99+
100+ exporter._handleUnpushedBranch(productseries)
101+
102+ # This completes successfully.
103+ transaction.commit()
104+
105
106 class TestExportToStackedBranch(TestCaseWithFactory):
107 """Test workaround for bzr bug 375013."""
108
109=== modified file 'lib/lp/translations/tests/test_autoapproval.py'
110--- lib/lp/translations/tests/test_autoapproval.py 2010-06-04 07:29:20 +0000
111+++ lib/lp/translations/tests/test_autoapproval.py 2010-06-07 10:42:25 +0000
112@@ -13,8 +13,7 @@
113 from contextlib import contextmanager
114 from datetime import datetime, timedelta
115 from pytz import UTC
116-import transaction
117-import unittest
118+from unittest import TestLoader
119
120 from zope.component import getUtility
121
122@@ -36,7 +35,6 @@
123 from lp.translations.interfaces.translationimportqueue import (
124 RosettaImportStatus, translation_import_queue_entry_age)
125 from lp.testing import TestCaseWithFactory
126-from lp.testing.factory import LaunchpadObjectFactory
127 from canonical.launchpad.webapp.testing import verifyObject
128 from canonical.testing import LaunchpadZopelessLayer
129
130@@ -46,31 +44,25 @@
131
132 Admittedly, this might be a little over-engineered but it looks good. ;)
133 """
134-
135- def _become(self, dbuser):
136- """Switch to a different db user."""
137- transaction.commit()
138- self.layer.switchDbUser(dbuser)
139-
140 def becomeTheGardener(self):
141 """One-way method to avoid unnecessary switch back."""
142- self._become('translations_import_queue_gardener')
143+ self.becomeDbUser('translations_import_queue_gardener')
144
145 @contextmanager
146 def beingTheGardener(self):
147 """Context manager to restore the launchpad user."""
148- self._become('translations_import_queue_gardener')
149+ self.becomeTheGardener()
150 yield
151- self._become('launchpad')
152-
153-
154-class TestCustomLanguageCode(unittest.TestCase):
155+ self.becomeDbUser('launchpad')
156+
157+
158+class TestCustomLanguageCode(TestCaseWithFactory):
159 """Unit tests for `CustomLanguageCode`."""
160
161 layer = LaunchpadZopelessLayer
162
163 def setUp(self):
164- self.factory = LaunchpadObjectFactory()
165+ super(TestCustomLanguageCode, self).setUp()
166 self.product_codes = {}
167 self.package_codes = {}
168
169@@ -149,8 +141,8 @@
170 self.assertEqual(Brazilian_code.language, Language.byCode('pt_BR'))
171
172
173-class TestGuessPOFileCustomLanguageCode(
174- unittest.TestCase, GardenerDbUserMixin):
175+class TestGuessPOFileCustomLanguageCode(TestCaseWithFactory,
176+ GardenerDbUserMixin):
177 """Test interaction with `TranslationImportQueueEntry.getGuessedPOFile`.
178
179 Auto-approval of translation files, i.e. figuring out which existing
180@@ -163,7 +155,7 @@
181 layer = LaunchpadZopelessLayer
182
183 def setUp(self):
184- self.factory = LaunchpadObjectFactory()
185+ super(TestGuessPOFileCustomLanguageCode, self).setUp()
186 self.product = self.factory.makeProduct()
187 self.series = self.factory.makeSeries(product=self.product)
188 self.queue = TranslationImportQueue()
189@@ -293,12 +285,12 @@
190 self.assertEqual(zh_TW_entry.getGuessedPOFile(), zh_CN_file)
191
192
193-class TestTemplateGuess(unittest.TestCase, GardenerDbUserMixin):
194+class TestTemplateGuess(TestCaseWithFactory, GardenerDbUserMixin):
195 """Test auto-approval's attempts to find the right template."""
196 layer = LaunchpadZopelessLayer
197
198 def setUp(self):
199- self.factory = LaunchpadObjectFactory()
200+ super(TestTemplateGuess, self).setUp()
201 self.templateset = POTemplateSet()
202
203 def _setUpProduct(self):
204@@ -596,7 +588,7 @@
205 self.assertEqual(template, entry.guessed_potemplate)
206
207
208-class TestKdePOFileGuess(unittest.TestCase, GardenerDbUserMixin):
209+class TestKdePOFileGuess(TestCaseWithFactory, GardenerDbUserMixin):
210 """Test auto-approval's `POFile` guessing for KDE uploads.
211
212 KDE has an unusual setup that the approver recognizes as a special
213@@ -609,10 +601,10 @@
214 layer = LaunchpadZopelessLayer
215
216 def setUp(self):
217- factory = LaunchpadObjectFactory()
218+ super(TestKdePOFileGuess, self).setUp()
219 self.queue = TranslationImportQueue()
220
221- self.distroseries = factory.makeDistroRelease()
222+ self.distroseries = self.factory.makeDistroRelease()
223
224 # For each of KDE3 and KDE4, set up:
225 # a translation package following that KDE's naming pattern,
226@@ -621,7 +613,7 @@
227 # a translation file into a language we'll test in.
228 self.kde_i18n_ca = SourcePackageNameSet().new('kde-i18n-ca')
229 kde3_package = SourcePackageNameSet().new('kde3')
230- ca_template = factory.makePOTemplate(
231+ ca_template = self.factory.makePOTemplate(
232 distroseries=self.distroseries,
233 sourcepackagename=kde3_package, name='kde3',
234 translation_domain='kde3')
235@@ -629,7 +621,7 @@
236
237 self.kde_l10n_nl = SourcePackageNameSet().new('kde-l10n-nl')
238 kde4_package = SourcePackageNameSet().new('kde4')
239- nl_template = factory.makePOTemplate(
240+ nl_template = self.factory.makePOTemplate(
241 distroseries=self.distroseries,
242 sourcepackagename=kde4_package, name='kde4',
243 translation_domain='kde4')
244@@ -925,4 +917,4 @@
245
246
247 def test_suite():
248- return unittest.TestLoader().loadTestsFromName(__name__)
249+ return TestLoader().loadTestsFromName(__name__)