~launchpad-pqm/launchpad/devel

12481.1.5 by Curtis Hovey
Updated sendExpirationWarningEmail documentation. Do not assert that the expiration date has past; silently return instead.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.17 by Karl Fogel
Add the copyright header block to the rest of the files under lib/lp/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3691.272.16 by Guilherme Salgado
A bunch of fixes and new tests suggested by Bjorn on his review
3
4
__metaclass__ = type
5
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
6
import bz2
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
7
from datetime import (
8
    datetime,
9
    timedelta,
10
    )
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
11
import os
12
import pickle
6010.1.1 by Guilherme Salgado
Change the check-teamparticipation.py script to check for people/teams which are not members of themselves.
13
import re
4868.1.6 by Guilherme Salgado
Turn the script that was supposed to fix invalid team participation entries into something that just reports the invalid ones, to be run from staging
14
import subprocess
14213.4.1 by Gavin Panella
Beginnings of performance optimization work for check-teamparticipation.py.
15
from unittest import TestLoader
16
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
17
from fixtures import TempDir
14213.4.1 by Gavin Panella
Beginnings of performance optimization work for check-teamparticipation.py.
18
import pytz
14213.4.5 by Gavin Panella
Improve the tests to add the logs as details.
19
from testtools.content import text_content
13841.3.1 by Ian Booth
Reduce queries used to find out team affiliation
20
from testtools.matchers import Equals
13130.1.12 by Curtis Hovey
Sorted imports.
21
import transaction
3691.272.16 by Guilherme Salgado
A bunch of fixes and new tests suggested by Bjorn on his review
22
from zope.component import getUtility
12481.1.1 by Curtis Hovey
Added a simple unittest to verify the behavioursendExpirationWarningEmail().
23
from zope.security.proxy import removeSecurityProxy
3691.272.16 by Guilherme Salgado
A bunch of fixes and new tests suggested by Bjorn on his review
24
13074.2.1 by j.c.sackett
Tests to produce the account relation error.
25
from canonical.config import config
4868.1.5 by Guilherme Salgado
Add an explicit test for ITeamMembershipSet.handleMembershipsExpiringToday()
26
from canonical.database.sqlbase import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
27
    cursor,
28
    flush_database_caches,
29
    flush_database_updates,
30
    sqlvalues,
31
    )
13074.2.1 by j.c.sackett
Tests to produce the account relation error.
32
from canonical.testing.layers import (
33
    DatabaseFunctionalLayer,
34
    LaunchpadZopelessLayer,
35
    )
13130.1.12 by Curtis Hovey
Sorted imports.
36
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
37
from lp.registry.interfaces.person import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
38
    IPersonSet,
12481.1.5 by Curtis Hovey
Updated sendExpirationWarningEmail documentation. Do not assert that the expiration date has past; silently return instead.
39
    TeamMembershipRenewalPolicy,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
40
    TeamSubscriptionPolicy,
41
    )
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
42
from lp.registry.interfaces.teammembership import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
43
    CyclicalTeamMembershipError,
44
    ITeamMembershipSet,
45
    TeamMembershipStatus,
46
    )
14213.4.1 by Gavin Panella
Beginnings of performance optimization work for check-teamparticipation.py.
47
from lp.registry.model.teammembership import (
13841.3.1 by Ian Booth
Reduce queries used to find out team affiliation
48
    find_team_participations,
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
49
    TeamMembership,
50
    TeamParticipation,
51
    )
14213.4.19 by Gavin Panella
Check that the only participant of a person is the person.
52
from lp.registry.scripts.teamparticipation import (
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
53
    check_teamparticipation_circular,
14213.4.19 by Gavin Panella
Check that the only participant of a person is the person.
54
    check_teamparticipation_consistency,
55
    ConsistencyError,
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
56
    fetch_team_participation_info,
14464.1.2 by Gavin Panella
Test for new function fix_teamparticipation_consistency().
57
    fix_teamparticipation_consistency,
14213.4.19 by Gavin Panella
Check that the only participant of a person is the person.
58
    )
14578.4.4 by William Grant
format-imports.
59
from lp.services.database.lpstorm import IStore
14213.4.3 by Gavin Panella
New test case TestCheckTeamParticipationScriptPerformance that runs check_teamparticipation() in-process, so we can monitor database activity more closely.
60
from lp.services.log.logger import BufferLogger
12481.1.1 by Curtis Hovey
Added a simple unittest to verify the behavioursendExpirationWarningEmail().
61
from lp.testing import (
14213.4.1 by Gavin Panella
Beginnings of performance optimization work for check-teamparticipation.py.
62
    login,
12617.1.6 by Curtis Hovey
Moved deactivateAllMembers tests to TestTeamMembershipSet.
63
    login_celebrity,
14213.4.1 by Gavin Panella
Beginnings of performance optimization work for check-teamparticipation.py.
64
    login_person,
12481.1.1 by Curtis Hovey
Added a simple unittest to verify the behavioursendExpirationWarningEmail().
65
    person_logged_in,
14213.4.1 by Gavin Panella
Beginnings of performance optimization work for check-teamparticipation.py.
66
    StormStatementRecorder,
14213.4.3 by Gavin Panella
New test case TestCheckTeamParticipationScriptPerformance that runs check_teamparticipation() in-process, so we can monitor database activity more closely.
67
    TestCase,
12481.1.1 by Curtis Hovey
Added a simple unittest to verify the behavioursendExpirationWarningEmail().
68
    TestCaseWithFactory,
14213.4.1 by Gavin Panella
Beginnings of performance optimization work for check-teamparticipation.py.
69
    )
14342.1.7 by William Grant
Kill a use of switchDbUser/reload_object.
70
from lp.testing.dbuser import dbuser
12481.1.1 by Curtis Hovey
Added a simple unittest to verify the behavioursendExpirationWarningEmail().
71
from lp.testing.mail_helpers import pop_notifications
13841.3.1 by Ian Booth
Reduce queries used to find out team affiliation
72
from lp.testing.matchers import HasQueryCount
14578.4.4 by William Grant
format-imports.
73
from lp.testing.systemdocs import (
74
    default_optionflags,
75
    LayeredDocFileSuite,
76
    setUp,
77
    tearDown,
78
    )
13074.2.1 by j.c.sackett
Tests to produce the account relation error.
79
80
81
class TestTeamMembershipSetScripts(TestCaseWithFactory):
82
    """Separate Testcase to separate out examples required dbuser switches.
83
84
    This uses the LaunchpadZoplelessLayer to provide layer.switchDbUser
85
    """
86
87
    layer = LaunchpadZopelessLayer
88
89
    def test_handleMembershipsExpiringToday_permissions(self):
90
        # Create two teams, a control team and and a team to be the control's
91
        # administrator.
92
        adminteam = self.factory.makeTeam()
93
        adminteam.setContactAddress(None)
94
        team = self.factory.makeTeam(owner=adminteam)
13074.2.5 by j.c.sackett
Changes from review.
95
        with person_logged_in(team.teamowner):
96
            team.renewal_policy = TeamMembershipRenewalPolicy.AUTOMATIC
97
            team.defaultrenewalperiod = 10
13074.2.1 by j.c.sackett
Tests to produce the account relation error.
98
99
        # Create a person to be in the control team.
100
        person = self.factory.makePerson()
101
        team.addMember(person, team.teamowner)
102
        membershipset = getUtility(ITeamMembershipSet)
13074.2.4 by j.c.sackett
Lint fixes
103
        teammembership = membershipset.getByPersonAndTeam(person, team)
13074.2.1 by j.c.sackett
Tests to produce the account relation error.
104
105
        # Set expiration time to now
106
        now = datetime.now(pytz.UTC)
107
        removeSecurityProxy(teammembership).dateexpires = now
108
109
        janitor = getUtility(ILaunchpadCelebrities).janitor
14342.1.7 by William Grant
Kill a use of switchDbUser/reload_object.
110
        with dbuser(config.expiredmembershipsflagger.dbuser):
111
            membershipset.handleMembershipsExpiringToday(janitor)
13074.2.1 by j.c.sackett
Tests to produce the account relation error.
112
        self.assertEqual(
113
            teammembership.status, TeamMembershipStatus.APPROVED)
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
114
13074.2.4 by j.c.sackett
Lint fixes
115
12617.1.6 by Curtis Hovey
Moved deactivateAllMembers tests to TestTeamMembershipSet.
116
class TestTeamMembershipSet(TestCaseWithFactory):
13074.2.1 by j.c.sackett
Tests to produce the account relation error.
117
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
118
    layer = DatabaseFunctionalLayer
3691.272.16 by Guilherme Salgado
A bunch of fixes and new tests suggested by Bjorn on his review
119
120
    def setUp(self):
12617.1.6 by Curtis Hovey
Moved deactivateAllMembers tests to TestTeamMembershipSet.
121
        super(TestTeamMembershipSet, self).setUp()
3691.272.16 by Guilherme Salgado
A bunch of fixes and new tests suggested by Bjorn on his review
122
        login('test@canonical.com')
123
        self.membershipset = getUtility(ITeamMembershipSet)
124
        self.personset = getUtility(IPersonSet)
125
126
    def test_membership_creation(self):
127
        marilize = self.personset.getByName('marilize')
128
        ubuntu_team = self.personset.getByName('ubuntu-team')
129
        membership = self.membershipset.new(
5825.2.3 by Guilherme Salgado
Fix the damn thing
130
            marilize, ubuntu_team, TeamMembershipStatus.APPROVED, marilize)
4868.1.1 by Guilherme Salgado
Make TeamMembership.setStatus call flush_db_updates() and add a script to fix the db corruption caused by the lack of flush_db_updates() on setStatus when it's called from the flag-expired-memberships script
131
        self.assertEqual(
132
            membership,
133
            self.membershipset.getByPersonAndTeam(marilize, ubuntu_team))
134
        self.assertEqual(membership.status, TeamMembershipStatus.APPROVED)
3691.272.16 by Guilherme Salgado
A bunch of fixes and new tests suggested by Bjorn on his review
135
5825.2.3 by Guilherme Salgado
Fix the damn thing
136
    def test_active_membership_creation_stores_proponent_and_reviewer(self):
5898.2.1 by Guilherme Salgado
Fix TeamMembershipSet.new() to store the datejoined when a membership is created in any active state.
137
        """Memberships created in any active state have the reviewer stored.
5825.2.3 by Guilherme Salgado
Fix the damn thing
138
5898.2.1 by Guilherme Salgado
Fix TeamMembershipSet.new() to store the datejoined when a membership is created in any active state.
139
        The date_joined, reviewer_comment, date_reviewed and attributes
140
        related to the proponent are also stored, but everything related to
141
        acknowledger will be left empty.
5825.2.3 by Guilherme Salgado
Fix the damn thing
142
        """
143
        marilize = self.personset.getByName('marilize')
144
        ubuntu_team = self.personset.getByName('ubuntu-team')
145
        membership = self.membershipset.new(
146
            marilize, ubuntu_team, TeamMembershipStatus.APPROVED,
147
            ubuntu_team.teamowner, comment="I like her")
148
        self.assertEqual(ubuntu_team.teamowner, membership.proposed_by)
149
        self.assertEqual(membership.proponent_comment, "I like her")
9719.1.1 by Brad Crittenden
Re-enable test_proponent_is_stored as the spurious error cannot be reproduced. Also drive-by clean up to use pytz.UTC.
150
        now = datetime.now(pytz.UTC)
5898.2.1 by Guilherme Salgado
Fix TeamMembershipSet.new() to store the datejoined when a membership is created in any active state.
151
        self.failUnless(membership.date_proposed <= now)
152
        self.failUnless(membership.datejoined <= now)
5825.2.3 by Guilherme Salgado
Fix the damn thing
153
        self.assertEqual(ubuntu_team.teamowner, membership.reviewed_by)
154
        self.assertEqual(membership.reviewer_comment, "I like her")
5898.2.1 by Guilherme Salgado
Fix TeamMembershipSet.new() to store the datejoined when a membership is created in any active state.
155
        self.failUnless(membership.date_reviewed <= now)
5825.2.3 by Guilherme Salgado
Fix the damn thing
156
        self.assertEqual(membership.acknowledged_by, None)
157
158
    def test_membership_creation_stores_proponent(self):
159
        """Memberships created in the proposed state have proponent stored.
160
161
        The proponent_comment and date_proposed are also stored, but
162
        everything related to reviewer and acknowledger will be left empty.
163
        """
164
        marilize = self.personset.getByName('marilize')
165
        ubuntu_team = self.personset.getByName('ubuntu-team')
166
        membership = self.membershipset.new(
167
            marilize, ubuntu_team, TeamMembershipStatus.PROPOSED, marilize,
168
            comment="I'd like to join")
169
        self.assertEqual(marilize, membership.proposed_by)
170
        self.assertEqual(membership.proponent_comment, "I'd like to join")
171
        self.failUnless(
9719.1.1 by Brad Crittenden
Re-enable test_proponent_is_stored as the spurious error cannot be reproduced. Also drive-by clean up to use pytz.UTC.
172
            membership.date_proposed <= datetime.now(pytz.UTC))
5825.2.3 by Guilherme Salgado
Fix the damn thing
173
        self.assertEqual(membership.reviewed_by, None)
174
        self.assertEqual(membership.acknowledged_by, None)
175
3691.272.18 by Guilherme Salgado
some final changes suggested by Bjorn
176
    def test_admin_membership_creation(self):
177
        ubuntu_team = self.personset.getByName('ubuntu-team')
3691.272.16 by Guilherme Salgado
A bunch of fixes and new tests suggested by Bjorn on his review
178
        no_priv = self.personset.getByName('no-priv')
179
        membership = self.membershipset.new(
5825.2.3 by Guilherme Salgado
Fix the damn thing
180
            no_priv, ubuntu_team, TeamMembershipStatus.ADMIN, no_priv)
4868.1.1 by Guilherme Salgado
Make TeamMembership.setStatus call flush_db_updates() and add a script to fix the db corruption caused by the lack of flush_db_updates() on setStatus when it's called from the flag-expired-memberships script
181
        self.assertEqual(
182
            membership,
183
            self.membershipset.getByPersonAndTeam(no_priv, ubuntu_team))
184
        self.assertEqual(membership.status, TeamMembershipStatus.ADMIN)
185
4868.1.5 by Guilherme Salgado
Add an explicit test for ITeamMembershipSet.handleMembershipsExpiringToday()
186
    def test_handleMembershipsExpiringToday(self):
187
        # Create a couple new teams, with one being a member of the other and
188
        # make Sample Person an approved member of both teams.
5126.3.16 by Edwin Grubbs
Fixed launchpad.Edit permission problem in a unit test
189
        login('foo.bar@canonical.com')
4868.1.5 by Guilherme Salgado
Add an explicit test for ITeamMembershipSet.handleMembershipsExpiringToday()
190
        foobar = self.personset.getByName('name16')
191
        sample_person = self.personset.getByName('name12')
192
        ubuntu_dev = self.personset.newTeam(
193
            foobar, 'ubuntu-dev', 'Ubuntu Developers')
194
        motu = self.personset.newTeam(foobar, 'motu', 'Ubuntu MOTU')
195
        ubuntu_dev.addMember(motu, foobar, force_team_add=True)
196
        ubuntu_dev.addMember(sample_person, foobar)
197
        motu.addMember(sample_person, foobar)
198
199
        # Now we need to cheat and set the expiration date of both memberships
200
        # manually because otherwise we would only be allowed to set an
201
        # expiration date in the future.
9719.1.1 by Brad Crittenden
Re-enable test_proponent_is_stored as the spurious error cannot be reproduced. Also drive-by clean up to use pytz.UTC.
202
        now = datetime.now(pytz.UTC)
4868.1.5 by Guilherme Salgado
Add an explicit test for ITeamMembershipSet.handleMembershipsExpiringToday()
203
        sample_person_on_motu = removeSecurityProxy(
204
            self.membershipset.getByPersonAndTeam(sample_person, motu))
205
        sample_person_on_motu.dateexpires = now
206
        sample_person_on_ubuntu_dev = removeSecurityProxy(
207
            self.membershipset.getByPersonAndTeam(sample_person, ubuntu_dev))
208
        sample_person_on_ubuntu_dev.dateexpires = now
209
        flush_database_updates()
210
        self.assertEqual(
211
            sample_person_on_ubuntu_dev.status, TeamMembershipStatus.APPROVED)
212
        self.assertEqual(
213
            sample_person_on_motu.status, TeamMembershipStatus.APPROVED)
214
        self.membershipset.handleMembershipsExpiringToday(foobar)
215
        flush_database_caches()
216
217
        # Now Sample Person is not direct nor indirect member of ubuntu-dev
218
        # or motu.
219
        self.assertEqual(
220
            sample_person_on_ubuntu_dev.status, TeamMembershipStatus.EXPIRED)
221
        self.failIf(sample_person.inTeam(ubuntu_dev))
222
        self.assertEqual(
223
            sample_person_on_motu.status, TeamMembershipStatus.EXPIRED)
224
        self.failIf(sample_person.inTeam(motu))
225
12617.1.7 by Curtis Hovey
Removed duplicate tests.
226
    def test_deactivateActiveMemberships(self):
12617.1.6 by Curtis Hovey
Moved deactivateAllMembers tests to TestTeamMembershipSet.
227
        superteam = self.factory.makeTeam(name='super')
228
        targetteam = self.factory.makeTeam(name='target')
12617.1.7 by Curtis Hovey
Removed duplicate tests.
229
        member = self.factory.makePerson()
12617.1.6 by Curtis Hovey
Moved deactivateAllMembers tests to TestTeamMembershipSet.
230
        login_celebrity('admin')
231
        targetteam.join(superteam, targetteam.teamowner)
12617.1.7 by Curtis Hovey
Removed duplicate tests.
232
        targetteam.addMember(member, targetteam.teamowner)
12617.1.6 by Curtis Hovey
Moved deactivateAllMembers tests to TestTeamMembershipSet.
233
        targetteam.teamowner.join(superteam, targetteam.teamowner)
12617.1.7 by Curtis Hovey
Removed duplicate tests.
234
        self.membershipset.deactivateActiveMemberships(
235
            targetteam, comment='test', reviewer=targetteam.teamowner)
236
        membership = self.membershipset.getByPersonAndTeam(member, targetteam)
237
        self.assertEqual('test', membership.last_change_comment)
238
        self.assertEqual(targetteam.teamowner, membership.last_changed_by)
239
        self.assertEqual([], list(targetteam.allmembers))
240
        self.assertEqual(
241
            [superteam], list(targetteam.teamowner.teams_participated_in))
242
        self.assertEqual([], list(member.teams_participated_in))
12617.1.6 by Curtis Hovey
Moved deactivateAllMembers tests to TestTeamMembershipSet.
243
4868.1.1 by Guilherme Salgado
Make TeamMembership.setStatus call flush_db_updates() and add a script to fix the db corruption caused by the lack of flush_db_updates() on setStatus when it's called from the flag-expired-memberships script
244
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
245
class TeamParticipationTestCase(TestCaseWithFactory):
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
246
    """Tests for team participation using 5 teams."""
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
247
    layer = DatabaseFunctionalLayer
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
248
249
    def setUp(self):
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
250
        super(TeamParticipationTestCase, self).setUp()
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
251
        login('foo.bar@canonical.com')
252
        person_set = getUtility(IPersonSet)
253
        self.foo_bar = person_set.getByEmail('foo.bar@canonical.com')
254
        self.no_priv = person_set.getByName('no-priv')
255
        self.team1 = person_set.newTeam(self.foo_bar, 'team1', 'team1')
256
        self.team2 = person_set.newTeam(self.foo_bar, 'team2', 'team2')
257
        self.team3 = person_set.newTeam(self.foo_bar, 'team3', 'team3')
258
        self.team4 = person_set.newTeam(self.foo_bar, 'team4', 'team4')
259
        self.team5 = person_set.newTeam(self.foo_bar, 'team5', 'team5')
260
261
    def assertParticipantsEquals(self, participant_names, team):
262
        """Assert that the participants names in team are the expected ones.
263
        """
264
        self.assertEquals(
265
            sorted(participant_names),
266
            sorted([participant.name for participant in team.allmembers]))
267
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
268
    def getTeamParticipationCount(self):
269
        return IStore(TeamParticipation).find(TeamParticipation).count()
270
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
271
13841.3.1 by Ian Booth
Reduce queries used to find out team affiliation
272
class TestTeamParticipationQuery(TeamParticipationTestCase):
273
    """A test case for teammembership.test_find_team_participations."""
274
275
    def test_find_team_participations(self):
276
        # The correct team participations are found and the query count is 1.
277
        self.team1.addMember(self.no_priv, self.foo_bar)
278
        self.team2.addMember(self.no_priv, self.foo_bar)
279
        self.team1.addMember(self.team2, self.foo_bar, force_team_add=True)
280
281
        people = [self.team1, self.team2]
282
        with StormStatementRecorder() as recorder:
283
            people_teams = find_team_participations(people)
284
        self.assertThat(recorder, HasQueryCount(Equals(1)))
285
        self.assertContentEqual([self.team1, self.team2], people_teams.keys())
286
        self.assertContentEqual([self.team1], people_teams[self.team1])
287
        self.assertContentEqual(
288
            [self.team1, self.team2], people_teams[self.team2])
289
290
    def test_find_team_participations_limited_teams(self):
291
        # The correct team participations are found and the query count is 1.
292
        self.team1.addMember(self.no_priv, self.foo_bar)
293
        self.team2.addMember(self.no_priv, self.foo_bar)
294
        self.team1.addMember(self.team2, self.foo_bar, force_team_add=True)
295
296
        people = [self.foo_bar, self.team2]
297
        teams = [self.team1, self.team2]
298
        with StormStatementRecorder() as recorder:
299
            people_teams = find_team_participations(people, teams)
300
        self.assertThat(recorder, HasQueryCount(Equals(1)))
301
        self.assertContentEqual(
302
            [self.foo_bar, self.team2], people_teams.keys())
303
        self.assertContentEqual(
304
            [self.team1, self.team2], people_teams[self.foo_bar])
305
        self.assertContentEqual(
306
            [self.team1, self.team2], people_teams[self.team2])
307
308
    def test_find_team_participations_no_query(self):
309
        # Check that no database query is made unless necessary.
310
        people = [self.foo_bar, self.team2]
311
        teams = [self.foo_bar]
312
        with StormStatementRecorder() as recorder:
313
            people_teams = find_team_participations(people, teams)
314
        self.assertThat(recorder, HasQueryCount(Equals(0)))
315
        self.assertContentEqual([self.foo_bar], people_teams.keys())
316
        self.assertContentEqual([self.foo_bar], people_teams[self.foo_bar])
317
318
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
319
class TestTeamParticipationHierarchy(TeamParticipationTestCase):
320
    """Participation management tests using 5 nested teams.
321
322
    Create a team hierarchy with 5 teams and one person (no-priv) as
323
    member of the last team in the chain.
324
        team1
325
           team2
326
              team3
327
                 team4
328
                    team5
329
                       no-priv
330
    """
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
331
    layer = DatabaseFunctionalLayer
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
332
333
    def setUp(self):
334
        """Setup the team hierarchy."""
335
        super(TestTeamParticipationHierarchy, self).setUp()
336
        self.team5.addMember(self.no_priv, self.foo_bar)
337
        self.team1.addMember(self.team2, self.foo_bar, force_team_add=True)
338
        self.team2.addMember(self.team3, self.foo_bar, force_team_add=True)
339
        self.team3.addMember(self.team4, self.foo_bar, force_team_add=True)
340
        self.team4.addMember(self.team5, self.foo_bar, force_team_add=True)
341
342
    def testTeamParticipationSetUp(self):
343
        """Make sure that the TeamParticipation are sane after setUp."""
344
        self.assertParticipantsEquals(
345
            ['name16', 'no-priv', 'team2', 'team3', 'team4', 'team5'],
346
            self.team1)
347
        self.assertParticipantsEquals(
348
            ['name16', 'no-priv', 'team3', 'team4', 'team5'], self.team2)
349
        self.assertParticipantsEquals(
350
            ['name16', 'no-priv', 'team4', 'team5'], self.team3)
351
        self.assertParticipantsEquals(
352
            ['name16', 'no-priv', 'team5'], self.team4)
353
        self.assertParticipantsEquals(
354
            ['name16', 'no-priv'], self.team5)
355
356
    def testSevereHierarchyByRemovingTeam3FromTeam2(self):
357
        """Make sure that the participations is updated correctly when
358
        the hierarchy is severed in the two.
359
360
        This is similar to what was experienced in bug 261915.
361
        """
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
362
        previous_count = self.getTeamParticipationCount()
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
363
        self.team2.setMembershipData(
364
            self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
6914.2.8 by Francis J. Lacoste
Make lint happy.
365
        self.assertParticipantsEquals(['name16', 'team2'], self.team1)
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
366
        self.assertParticipantsEquals(['name16'], self.team2)
367
        self.assertParticipantsEquals(
368
            ['name16', 'no-priv', 'team4', 'team5'], self.team3)
369
        self.assertParticipantsEquals(
370
            ['name16', 'no-priv', 'team5'], self.team4)
371
        self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
372
        self.assertEqual(
13785.7.1 by Aaron Bentley
Allow inviting previously-declined members.
373
            previous_count - 8,
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
374
            self.getTeamParticipationCount())
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
375
376
    def testRemovingLeafTeam(self):
377
        """Make sure that participations are updated correctly when removing
378
        the leaf team.
379
        """
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
380
        previous_count = self.getTeamParticipationCount()
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
381
        self.team4.setMembershipData(
382
            self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
383
        self.assertParticipantsEquals(
384
            ['name16', 'team2', 'team3', 'team4'], self.team1)
385
        self.assertParticipantsEquals(
386
            ['name16', 'team3', 'team4'], self.team2)
387
        self.assertParticipantsEquals(['name16', 'team4'], self.team3)
388
        self.assertParticipantsEquals(['name16'], self.team4)
389
        self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
390
        self.assertEqual(
13785.7.1 by Aaron Bentley
Allow inviting previously-declined members.
391
            previous_count - 8,
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
392
            self.getTeamParticipationCount())
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
393
394
395
class TestTeamParticipationTree(TeamParticipationTestCase):
396
    """Participation management tests using 5 nested teams
397
398
    Create a team hierarchy looking like this:
399
        team1
400
           team2
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
401
              team5
402
              team3
403
                 team4
404
                    team5
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
405
                       no-priv
406
    """
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
407
    layer = DatabaseFunctionalLayer
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
408
409
    def setUp(self):
410
        """Setup the team hierarchy."""
411
        super(TestTeamParticipationTree, self).setUp()
412
        self.team5.addMember(self.no_priv, self.foo_bar)
413
        self.team1.addMember(self.team2, self.foo_bar, force_team_add=True)
414
        self.team2.addMember(self.team3, self.foo_bar, force_team_add=True)
415
        self.team2.addMember(self.team5, self.foo_bar, force_team_add=True)
416
        self.team3.addMember(self.team4, self.foo_bar, force_team_add=True)
417
        self.team4.addMember(self.team5, self.foo_bar, force_team_add=True)
418
12019.11.5 by Brad Crittenden
Borked checkpoint
419
    def tearDown(self):
420
        super(TestTeamParticipationTree, self).tearDown()
421
        self.layer.force_dirty_database()
422
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
423
    def testTeamParticipationSetUp(self):
424
        """Make sure that the TeamParticipation are sane after setUp."""
425
        self.assertParticipantsEquals(
426
            ['name16', 'no-priv', 'team2', 'team3', 'team4', 'team5'],
427
            self.team1)
428
        self.assertParticipantsEquals(
429
            ['name16', 'no-priv', 'team3', 'team4', 'team5'], self.team2)
430
        self.assertParticipantsEquals(
431
            ['name16', 'no-priv', 'team4', 'team5'], self.team3)
432
        self.assertParticipantsEquals(
433
            ['name16', 'no-priv', 'team5'], self.team4)
434
        self.assertParticipantsEquals(
435
            ['name16', 'no-priv'], self.team5)
436
437
    def testRemoveTeam3FromTeam2(self):
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
438
        previous_count = self.getTeamParticipationCount()
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
439
        self.team2.setMembershipData(
440
            self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
441
        self.assertParticipantsEquals(
442
            ['name16', 'no-priv', 'team2', 'team5'], self.team1)
443
        self.assertParticipantsEquals(
444
            ['name16', 'no-priv', 'team5'], self.team2)
445
        self.assertParticipantsEquals(
446
            ['name16', 'no-priv', 'team4', 'team5'], self.team3)
447
        self.assertParticipantsEquals(
448
            ['name16', 'no-priv', 'team5'], self.team4)
449
        self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
450
        self.assertEqual(
13785.7.1 by Aaron Bentley
Allow inviting previously-declined members.
451
            previous_count - 4,
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
452
            self.getTeamParticipationCount())
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
453
454
    def testRemoveTeam5FromTeam4(self):
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
455
        previous_count = self.getTeamParticipationCount()
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
456
        self.team4.setMembershipData(
457
            self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
458
        self.assertParticipantsEquals(
459
            ['name16', 'no-priv', 'team2', 'team3', 'team4', 'team5'],
460
            self.team1)
461
        self.assertParticipantsEquals(
6914.2.5 by Francis J. Lacoste
Use correct expected values
462
            ['name16', 'no-priv', 'team3', 'team4', 'team5'], self.team2)
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
463
        self.assertParticipantsEquals(
6914.2.5 by Francis J. Lacoste
Use correct expected values
464
            ['name16', 'team4'], self.team3)
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
465
        self.assertParticipantsEquals(['name16'], self.team4)
466
        self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
467
        self.assertEqual(
13785.7.1 by Aaron Bentley
Allow inviting previously-declined members.
468
            previous_count - 4,
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
469
            self.getTeamParticipationCount())
6914.2.3 by Francis J. Lacoste
Refactored tests to use a common setUp and assert, and add more tests on a more complex structure.
470
471
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
472
class TestParticipationCleanup(TeamParticipationTestCase):
473
    """Test deletion of a member from a team with many superteams.
474
    Create a team hierarchy looking like this:
475
        team1
476
           team2
477
              team3
478
                 team4
479
                    team5
480
                       no-priv
481
    """
482
483
    def setUp(self):
484
        """Setup the team hierarchy."""
485
        super(TestParticipationCleanup, self).setUp()
486
        self.team1.addMember(self.team2, self.foo_bar, force_team_add=True)
487
        self.team2.addMember(self.team3, self.foo_bar, force_team_add=True)
488
        self.team3.addMember(self.team4, self.foo_bar, force_team_add=True)
489
        self.team4.addMember(self.team5, self.foo_bar, force_team_add=True)
490
        self.team5.addMember(self.no_priv, self.foo_bar)
491
492
    def testMemberRemoval(self):
493
        """Remove the member from the last team.
494
495
        The number of db queries should be constant not O(depth).
496
        """
497
        self.assertStatementCount(
12156.7.6 by Edwin Grubbs
Working recursive query.
498
            7,
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
499
            self.team5.setMembershipData, self.no_priv,
12156.7.4 by Edwin Grubbs
Mostly working new algorithm.
500
            TeamMembershipStatus.DEACTIVATED, self.team5.teamowner)
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
501
502
6914.2.4 by Francis J. Lacoste
Add more complex structure for more removeal tests.
503
class TestTeamParticipationMesh(TeamParticipationTestCase):
504
    """Participation management tests using two roots and some duplicated
505
    branches.
506
507
    Create a team hierarchy looking like this:
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
508
        team1     team6
509
           \     /  |
510
            team2   |
511
           /    |   |
512
        team3   |   |
513
             \  |  /
514
              team4
515
                 team5
516
                    no-priv
6914.2.4 by Francis J. Lacoste
Add more complex structure for more removeal tests.
517
    """
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
518
    layer = DatabaseFunctionalLayer
6914.2.4 by Francis J. Lacoste
Add more complex structure for more removeal tests.
519
520
    def setUp(self):
521
        """Setup the team hierarchy."""
522
        super(TestTeamParticipationMesh, self).setUp()
523
        self.team6 = getUtility(IPersonSet).newTeam(
524
            self.foo_bar, 'team6', 'team6')
525
        self.team5.addMember(self.no_priv, self.foo_bar)
526
        self.team1.addMember(self.team2, self.foo_bar, force_team_add=True)
527
        self.team2.addMember(self.team3, self.foo_bar, force_team_add=True)
528
        self.team2.addMember(self.team4, self.foo_bar, force_team_add=True)
529
        self.team3.addMember(self.team4, self.foo_bar, force_team_add=True)
530
        self.team4.addMember(self.team5, self.foo_bar, force_team_add=True)
531
        self.team6.addMember(self.team2, self.foo_bar, force_team_add=True)
532
        self.team6.addMember(self.team4, self.foo_bar, force_team_add=True)
533
12019.11.5 by Brad Crittenden
Borked checkpoint
534
    def tearDown(self):
535
        super(TestTeamParticipationMesh, self).tearDown()
536
        self.layer.force_dirty_database()
537
6914.2.4 by Francis J. Lacoste
Add more complex structure for more removeal tests.
538
    def testTeamParticipationSetUp(self):
539
        """Make sure that the TeamParticipation are sane after setUp."""
540
        self.assertParticipantsEquals(
541
            ['name16', 'no-priv', 'team2', 'team3', 'team4', 'team5'],
542
            self.team1)
543
        self.assertParticipantsEquals(
544
            ['name16', 'no-priv', 'team3', 'team4', 'team5'], self.team2)
545
        self.assertParticipantsEquals(
546
            ['name16', 'no-priv', 'team4', 'team5'], self.team3)
547
        self.assertParticipantsEquals(
548
            ['name16', 'no-priv', 'team5'], self.team4)
549
        self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
550
        self.assertParticipantsEquals(
551
            ['name16', 'no-priv', 'team2', 'team3', 'team4', 'team5'],
552
            self.team6)
553
554
    def testRemoveTeam3FromTeam2(self):
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
555
        previous_count = self.getTeamParticipationCount()
6914.2.4 by Francis J. Lacoste
Add more complex structure for more removeal tests.
556
        self.team2.setMembershipData(
557
            self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
558
        self.assertParticipantsEquals(
559
            ['name16', 'no-priv', 'team2', 'team4', 'team5'], self.team1)
560
        self.assertParticipantsEquals(
561
            ['name16', 'no-priv', 'team4', 'team5'], self.team2)
562
        self.assertParticipantsEquals(
563
            ['name16', 'no-priv', 'team4', 'team5'], self.team3)
564
        self.assertParticipantsEquals(
565
            ['name16', 'no-priv', 'team5'], self.team4)
566
        self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
567
        self.assertParticipantsEquals(
568
            ['name16', 'no-priv', 'team2', 'team4', 'team5'], self.team6)
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
569
        self.assertEqual(
13785.7.1 by Aaron Bentley
Allow inviting previously-declined members.
570
            previous_count - 3,
12156.7.9 by Edwin Grubbs
Added tests for side-effects.
571
            self.getTeamParticipationCount())
6914.2.4 by Francis J. Lacoste
Add more complex structure for more removeal tests.
572
12617.1.7 by Curtis Hovey
Removed duplicate tests.
573
    def testRemoveTeam5FromTeam4(self):
12617.1.1 by Curtis Hovey
Replace bad independent implementation of TeamPartcipation cleaning with a
574
        previous_count = self.getTeamParticipationCount()
12617.1.7 by Curtis Hovey
Removed duplicate tests.
575
        self.team4.setMembershipData(
576
            self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
577
        self.assertParticipantsEquals(
578
            ['name16', 'team2', 'team3', 'team4'], self.team1)
579
        self.assertParticipantsEquals(
580
            ['name16', 'team3', 'team4'], self.team2)
581
        self.assertParticipantsEquals(['name16', 'team4'], self.team3)
582
        self.assertParticipantsEquals(['name16'], self.team4)
12617.1.1 by Curtis Hovey
Replace bad independent implementation of TeamPartcipation cleaning with a
583
        self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
584
        self.assertParticipantsEquals(
12617.1.7 by Curtis Hovey
Removed duplicate tests.
585
            ['name16', 'team2', 'team3', 'team4'], self.team6)
586
        self.assertEqual(
13785.7.1 by Aaron Bentley
Allow inviting previously-declined members.
587
            previous_count - 10,
12617.1.7 by Curtis Hovey
Removed duplicate tests.
588
            self.getTeamParticipationCount())
12617.1.1 by Curtis Hovey
Replace bad independent implementation of TeamPartcipation cleaning with a
589
12617.1.7 by Curtis Hovey
Removed duplicate tests.
590
    def testTeam3_deactivateActiveMemberships(self):
12617.1.1 by Curtis Hovey
Replace bad independent implementation of TeamPartcipation cleaning with a
591
        # Removing all the members of team2 will not remove memberships
592
        # to super teams from other paths.
593
        non_member = self.factory.makePerson()
594
        self.team3.addMember(non_member, self.foo_bar, force_team_add=True)
595
        previous_count = self.getTeamParticipationCount()
12617.1.3 by Curtis Hovey
Moved deactivateAllMembers to ITeamMembershipSet.deactivateActiveMemberships.
596
        membershipset = getUtility(ITeamMembershipSet)
597
        membershipset.deactivateActiveMemberships(
598
            self.team3, 'gone', self.foo_bar)
12617.1.7 by Curtis Hovey
Removed duplicate tests.
599
        self.assertEqual([], list(self.team3.allmembers))
12617.1.1 by Curtis Hovey
Replace bad independent implementation of TeamPartcipation cleaning with a
600
        self.assertParticipantsEquals(
601
            ['name16', 'no-priv', 'team2', 'team3', 'team4', 'team5'],
602
            self.team1)
603
        self.assertParticipantsEquals(
604
            ['name16', 'no-priv', 'team3', 'team4', 'team5'], self.team2)
605
        self.assertParticipantsEquals(
606
            [], self.team3)
607
        self.assertParticipantsEquals(
608
            ['name16', 'no-priv', 'team5'], self.team4)
609
        self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
610
        self.assertParticipantsEquals(
611
            ['name16', 'no-priv', 'team2', 'team3', 'team4', 'team5'],
612
            self.team6)
613
        self.assertEqual(previous_count - 8, self.getTeamParticipationCount())
614
6914.2.4 by Francis J. Lacoste
Add more complex structure for more removeal tests.
615
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
616
class TestTeamMembership(TestCaseWithFactory):
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
617
    layer = DatabaseFunctionalLayer
4868.1.1 by Guilherme Salgado
Make TeamMembership.setStatus call flush_db_updates() and add a script to fix the db corruption caused by the lack of flush_db_updates() on setStatus when it's called from the flag-expired-memberships script
618
6809.1.1 by Guilherme Salgado
Fix bug 248498
619
    def test_teams_not_kicked_from_themselves_bug_248498(self):
620
        """The self-participation of a team must not be removed.
621
622
        Performing the following steps would cause a team's self-participation
623
        to be removed, but it shouldn't.
624
625
            1. propose team A as a member of team B
626
            2. propose team B as a member of team A
627
            3. approve team A as a member of team B
628
            4. decline team B as a member of team A
629
630
        This test will make sure that doesn't happen in the future.
631
        """
632
        login('test@canonical.com')
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
633
        person = self.factory.makePerson()
13785.7.1 by Aaron Bentley
Allow inviting previously-declined members.
634
        login_person(person)  # Now login with the future owner of the teams.
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
635
        teamA = self.factory.makeTeam(
6809.1.1 by Guilherme Salgado
Fix bug 248498
636
            person, subscription_policy=TeamSubscriptionPolicy.MODERATED)
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
637
        teamB = self.factory.makeTeam(
6809.1.1 by Guilherme Salgado
Fix bug 248498
638
            person, subscription_policy=TeamSubscriptionPolicy.MODERATED)
639
        self.failUnless(
640
            teamA.inTeam(teamA), "teamA is not a participant of itself")
641
        self.failUnless(
642
            teamB.inTeam(teamB), "teamB is not a participant of itself")
643
644
        teamA.join(teamB, requester=person)
645
        teamB.join(teamA, requester=person)
646
        teamB.setMembershipData(teamA, TeamMembershipStatus.APPROVED, person)
647
        teamA.setMembershipData(teamB, TeamMembershipStatus.DECLINED, person)
648
649
        self.failUnless(teamA.hasParticipationEntryFor(teamA),
650
                        "teamA is not a participant of itself")
651
        self.failUnless(teamB.hasParticipationEntryFor(teamB),
652
                        "teamB is not a participant of itself")
653
4868.1.1 by Guilherme Salgado
Make TeamMembership.setStatus call flush_db_updates() and add a script to fix the db corruption caused by the lack of flush_db_updates() on setStatus when it's called from the flag-expired-memberships script
654
    def test_membership_status_changes_are_immediately_flushed_to_db(self):
655
        """Any changes to a membership status must be imediately flushed.
656
657
        Sometimes we may change multiple team memberships in the same
658
        transaction (e.g. when expiring memberships). If there are multiple
659
        memberships for a given member changed in this way, we need to
660
        ensure each change is flushed to the database so that subsequent ones
661
        operate on the correct data.
662
        """
663
        login('foo.bar@canonical.com')
4868.1.2 by Guilherme Salgado
A couple small changes suggested by kiko
664
        tm = TeamMembership.selectFirstBy(
665
            status=TeamMembershipStatus.APPROVED, orderBy='id')
666
        tm.setStatus(TeamMembershipStatus.DEACTIVATED,
667
                     getUtility(IPersonSet).getByName('name16'))
668
        # Bypass SQLObject to make sure the update was really flushed to the
669
        # database.
670
        cur = cursor()
671
        cur.execute("SELECT status FROM teammembership WHERE id = %d" % tm.id)
672
        [new_status] = cur.fetchone()
673
        self.assertEqual(new_status, TeamMembershipStatus.DEACTIVATED.value)
3691.272.16 by Guilherme Salgado
A bunch of fixes and new tests suggested by Bjorn on his review
674
675
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
676
class TestTeamMembershipSetStatus(TestCaseWithFactory):
5825.2.3 by Guilherme Salgado
Fix the damn thing
677
    """Test the behaviour of TeamMembership's setStatus()."""
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
678
    layer = DatabaseFunctionalLayer
5825.2.3 by Guilherme Salgado
Fix the damn thing
679
680
    def setUp(self):
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
681
        super(TestTeamMembershipSetStatus, self).setUp()
5825.2.3 by Guilherme Salgado
Fix the damn thing
682
        login('foo.bar@canonical.com')
683
        self.foobar = getUtility(IPersonSet).getByName('name16')
684
        self.no_priv = getUtility(IPersonSet).getByName('no-priv')
685
        self.ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')
686
        self.admins = getUtility(IPersonSet).getByName('admins')
6592.3.1 by Guilherme Salgado
change TeamMembership.setStatus() to allow all status to be transitioned to admin.
687
        # Create a bunch of arbitrary teams to use in the tests.
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
688
        self.team1 = self.factory.makeTeam(self.foobar)
689
        self.team2 = self.factory.makeTeam(self.foobar)
690
        self.team3 = self.factory.makeTeam(self.foobar)
5825.2.3 by Guilherme Salgado
Fix the damn thing
691
9719.1.1 by Brad Crittenden
Re-enable test_proponent_is_stored as the spurious error cannot be reproduced. Also drive-by clean up to use pytz.UTC.
692
    def test_proponent_is_stored(self):
9538.1.2 by Gary Poster
disable unit test more according to proper process
693
        for status in [TeamMembershipStatus.DEACTIVATED,
694
                       TeamMembershipStatus.EXPIRED,
695
                       TeamMembershipStatus.DECLINED]:
696
            tm = TeamMembership(
697
                person=self.no_priv, team=self.ubuntu_team, status=status)
698
            self.failIf(
699
                tm.proposed_by, "There can be no proponent at this point.")
700
            self.failIf(
701
                tm.date_proposed, "There can be no proposed date this point.")
702
            self.failIf(tm.proponent_comment,
703
                        "There can be no proponent comment at this point.")
704
            tm.setStatus(
705
                TeamMembershipStatus.PROPOSED, self.foobar,
706
                "Did it 'cause I can")
707
            self.failUnlessEqual(tm.proposed_by, self.foobar)
708
            self.failUnlessEqual(tm.proponent_comment, "Did it 'cause I can")
709
            self.failUnless(
9719.1.1 by Brad Crittenden
Re-enable test_proponent_is_stored as the spurious error cannot be reproduced. Also drive-by clean up to use pytz.UTC.
710
                tm.date_proposed <= datetime.now(pytz.UTC))
9538.1.2 by Gary Poster
disable unit test more according to proper process
711
            # Destroy the membership so that we can create another in a
712
            # different state.
713
            tm.destroySelf()
5825.2.3 by Guilherme Salgado
Fix the damn thing
714
715
    def test_acknowledger_is_stored(self):
716
        for status in [TeamMembershipStatus.APPROVED,
717
                       TeamMembershipStatus.INVITATION_DECLINED]:
718
            tm = TeamMembership(
719
                person=self.admins, team=self.ubuntu_team,
720
                status=TeamMembershipStatus.INVITED)
721
            self.failIf(
722
                tm.acknowledged_by,
723
                "There can be no acknowledger at this point.")
724
            self.failIf(
725
                tm.date_acknowledged,
726
                "There can be no accepted date this point.")
727
            self.failIf(tm.acknowledger_comment,
728
                        "There can be no acknowledger comment at this point.")
729
            tm.setStatus(status, self.foobar, "Did it 'cause I can")
730
            self.failUnlessEqual(tm.acknowledged_by, self.foobar)
5825.2.8 by Guilherme Salgado
fix a couple things spotted by make lint
731
            self.failUnlessEqual(
732
                tm.acknowledger_comment, "Did it 'cause I can")
5825.2.3 by Guilherme Salgado
Fix the damn thing
733
            self.failUnless(
9719.1.1 by Brad Crittenden
Re-enable test_proponent_is_stored as the spurious error cannot be reproduced. Also drive-by clean up to use pytz.UTC.
734
                tm.date_acknowledged <= datetime.now(pytz.UTC))
5825.2.3 by Guilherme Salgado
Fix the damn thing
735
            # Destroy the membership so that we can create another in a
736
            # different state.
737
            tm.destroySelf()
738
739
    def test_reviewer_is_stored(self):
740
        transitions_mapping = {
741
            TeamMembershipStatus.DEACTIVATED: [TeamMembershipStatus.APPROVED],
742
            TeamMembershipStatus.EXPIRED: [TeamMembershipStatus.APPROVED],
743
            TeamMembershipStatus.PROPOSED: [
744
                TeamMembershipStatus.APPROVED, TeamMembershipStatus.DECLINED],
745
            TeamMembershipStatus.DECLINED: [TeamMembershipStatus.APPROVED],
746
            TeamMembershipStatus.INVITATION_DECLINED: [
747
                TeamMembershipStatus.APPROVED]}
748
        for status, new_statuses in transitions_mapping.items():
749
            for new_status in new_statuses:
750
                tm = TeamMembership(
751
                    person=self.no_priv, team=self.ubuntu_team, status=status)
752
                self.failIf(
753
                    tm.reviewed_by,
754
                    "There can be no approver at this point.")
755
                self.failIf(
756
                    tm.date_reviewed,
757
                    "There can be no approved date this point.")
758
                self.failIf(
759
                    tm.reviewer_comment,
760
                    "There can be no approver comment at this point.")
761
                tm.setStatus(new_status, self.foobar, "Did it 'cause I can")
762
                self.failUnlessEqual(tm.reviewed_by, self.foobar)
5825.2.8 by Guilherme Salgado
fix a couple things spotted by make lint
763
                self.failUnlessEqual(
764
                    tm.reviewer_comment, "Did it 'cause I can")
5825.2.3 by Guilherme Salgado
Fix the damn thing
765
                self.failUnless(
9719.1.1 by Brad Crittenden
Re-enable test_proponent_is_stored as the spurious error cannot be reproduced. Also drive-by clean up to use pytz.UTC.
766
                    tm.date_reviewed <= datetime.now(pytz.UTC))
5825.2.3 by Guilherme Salgado
Fix the damn thing
767
768
                # Destroy the membership so that we can create another in a
769
                # different state.
770
                tm.destroySelf()
771
772
    def test_datejoined(self):
773
        """TeamMembership.datejoined stores the date in which this membership
774
        was made active for the first time.
775
        """
776
        tm = TeamMembership(
777
            person=self.no_priv, team=self.ubuntu_team,
778
            status=TeamMembershipStatus.PROPOSED)
779
        self.failIf(
780
            tm.datejoined, "There can be no datejoined at this point.")
781
        tm.setStatus(TeamMembershipStatus.APPROVED, self.foobar)
9719.1.1 by Brad Crittenden
Re-enable test_proponent_is_stored as the spurious error cannot be reproduced. Also drive-by clean up to use pytz.UTC.
782
        now = datetime.now(pytz.UTC)
5825.2.3 by Guilherme Salgado
Fix the damn thing
783
        self.failUnless(tm.datejoined <= now)
784
785
        # We now set the status to deactivated and change datejoined to a
786
        # date in the past just so that we can easily show it's not changed
787
        # again by setStatus().
788
        one_minute_ago = now - timedelta(minutes=1)
789
        tm.setStatus(TeamMembershipStatus.DEACTIVATED, self.foobar)
790
        tm.datejoined = one_minute_ago
791
        tm.setStatus(TeamMembershipStatus.APPROVED, self.foobar)
792
        self.failUnless(tm.datejoined <= one_minute_ago)
793
6220.2.1 by Guilherme Salgado
Fix the bug.
794
    def test_no_cyclical_membership_allowed(self):
795
        """No status change can create cyclical memberships."""
796
        # Invite team2 as member of team1 and team1 as member of team2. This
797
        # is not a problem because that won't make any team an active member
798
        # of the other.
10002.2.26 by Edwin Grubbs
Fixed tests and the message from +addmember.
799
        self.team1.addMember(self.team2, self.no_priv)
800
        self.team2.addMember(self.team1, self.no_priv)
6220.2.1 by Guilherme Salgado
Fix the bug.
801
        team1_on_team2 = getUtility(ITeamMembershipSet).getByPersonAndTeam(
6592.3.1 by Guilherme Salgado
change TeamMembership.setStatus() to allow all status to be transitioned to admin.
802
            self.team1, self.team2)
6220.2.1 by Guilherme Salgado
Fix the bug.
803
        team2_on_team1 = getUtility(ITeamMembershipSet).getByPersonAndTeam(
6592.3.1 by Guilherme Salgado
change TeamMembership.setStatus() to allow all status to be transitioned to admin.
804
            self.team2, self.team1)
6220.2.1 by Guilherme Salgado
Fix the bug.
805
        self.failUnlessEqual(
806
            team1_on_team2.status, TeamMembershipStatus.INVITED)
807
        self.failUnlessEqual(
808
            team2_on_team1.status, TeamMembershipStatus.INVITED)
809
810
        # Now make team1 an active member of team2.  From this point onwards,
811
        # team2 cannot be made an active member of team1.
6592.3.1 by Guilherme Salgado
change TeamMembership.setStatus() to allow all status to be transitioned to admin.
812
        team1_on_team2.setStatus(TeamMembershipStatus.APPROVED, self.foobar)
6220.2.1 by Guilherme Salgado
Fix the bug.
813
        flush_database_updates()
814
        self.failUnlessEqual(
815
            team1_on_team2.status, TeamMembershipStatus.APPROVED)
816
        self.assertRaises(
817
            CyclicalTeamMembershipError, team2_on_team1.setStatus,
6592.3.1 by Guilherme Salgado
change TeamMembership.setStatus() to allow all status to be transitioned to admin.
818
            TeamMembershipStatus.APPROVED, self.foobar)
6220.2.1 by Guilherme Salgado
Fix the bug.
819
        self.failUnlessEqual(
820
            team2_on_team1.status, TeamMembershipStatus.INVITED)
821
822
        # It is possible to change the state of team2's membership on team1
823
        # to another inactive state, though.
824
        team2_on_team1.setStatus(
6592.3.1 by Guilherme Salgado
change TeamMembership.setStatus() to allow all status to be transitioned to admin.
825
            TeamMembershipStatus.INVITATION_DECLINED, self.foobar)
6220.2.1 by Guilherme Salgado
Fix the bug.
826
        self.failUnlessEqual(
827
            team2_on_team1.status, TeamMembershipStatus.INVITATION_DECLINED)
828
6220.2.2 by Guilherme Salgado
Fix to prevent cyclical participation as well.
829
    def test_no_cyclical_participation_allowed(self):
830
        """No status change can create cyclical participation."""
831
        # Invite team1 as a member of team3 and forcibly add team2 as member
832
        # of team1 and team3 as member of team2.
10002.2.26 by Edwin Grubbs
Fixed tests and the message from +addmember.
833
        self.team3.addMember(self.team1, self.no_priv)
6592.3.1 by Guilherme Salgado
change TeamMembership.setStatus() to allow all status to be transitioned to admin.
834
        self.team1.addMember(self.team2, self.foobar, force_team_add=True)
835
        self.team2.addMember(self.team3, self.foobar, force_team_add=True)
6220.2.2 by Guilherme Salgado
Fix to prevent cyclical participation as well.
836
837
        # Since team2 is a member of team1 and team3 is a member of team2, we
838
        # can't make team1 a member of team3.
839
        team1_on_team3 = getUtility(ITeamMembershipSet).getByPersonAndTeam(
6592.3.1 by Guilherme Salgado
change TeamMembership.setStatus() to allow all status to be transitioned to admin.
840
            self.team1, self.team3)
6220.2.2 by Guilherme Salgado
Fix to prevent cyclical participation as well.
841
        self.assertRaises(
842
            CyclicalTeamMembershipError, team1_on_team3.setStatus,
6592.3.1 by Guilherme Salgado
change TeamMembership.setStatus() to allow all status to be transitioned to admin.
843
            TeamMembershipStatus.APPROVED, self.foobar)
844
845
    def test_invited_member_can_be_made_admin(self):
10002.2.26 by Edwin Grubbs
Fixed tests and the message from +addmember.
846
        self.team2.addMember(self.team1, self.no_priv)
6592.3.1 by Guilherme Salgado
change TeamMembership.setStatus() to allow all status to be transitioned to admin.
847
        team1_on_team2 = getUtility(ITeamMembershipSet).getByPersonAndTeam(
848
            self.team1, self.team2)
849
        self.assertEqual(team1_on_team2.status, TeamMembershipStatus.INVITED)
850
        team1_on_team2.setStatus(TeamMembershipStatus.ADMIN, self.foobar)
851
        self.assertEqual(team1_on_team2.status, TeamMembershipStatus.ADMIN)
852
853
    def test_deactivated_member_can_be_made_admin(self):
854
        self.team2.addMember(self.team1, self.foobar, force_team_add=True)
855
        team1_on_team2 = getUtility(ITeamMembershipSet).getByPersonAndTeam(
856
            self.team1, self.team2)
857
        self.assertEqual(team1_on_team2.status, TeamMembershipStatus.APPROVED)
6809.1.2 by Guilherme Salgado
A couple fixes suggested by Graham.
858
        team1_on_team2.setStatus(
859
            TeamMembershipStatus.DEACTIVATED, self.foobar)
6592.3.1 by Guilherme Salgado
change TeamMembership.setStatus() to allow all status to be transitioned to admin.
860
        self.assertEqual(
861
            team1_on_team2.status, TeamMembershipStatus.DEACTIVATED)
862
        team1_on_team2.setStatus(TeamMembershipStatus.ADMIN, self.foobar)
863
        self.assertEqual(team1_on_team2.status, TeamMembershipStatus.ADMIN)
864
865
    def test_expired_member_can_be_made_admin(self):
866
        self.team2.addMember(self.team1, self.foobar, force_team_add=True)
867
        team1_on_team2 = getUtility(ITeamMembershipSet).getByPersonAndTeam(
868
            self.team1, self.team2)
869
        self.assertEqual(team1_on_team2.status, TeamMembershipStatus.APPROVED)
870
        team1_on_team2.setStatus(TeamMembershipStatus.EXPIRED, self.foobar)
871
        self.assertEqual(team1_on_team2.status, TeamMembershipStatus.EXPIRED)
872
        team1_on_team2.setStatus(TeamMembershipStatus.ADMIN, self.foobar)
873
        self.assertEqual(team1_on_team2.status, TeamMembershipStatus.ADMIN)
874
875
    def test_declined_member_can_be_made_admin(self):
876
        self.team2.subscriptionpolicy = TeamSubscriptionPolicy.MODERATED
877
        self.team1.join(self.team2, requester=self.foobar)
878
        team1_on_team2 = getUtility(ITeamMembershipSet).getByPersonAndTeam(
879
            self.team1, self.team2)
880
        self.assertEqual(team1_on_team2.status, TeamMembershipStatus.PROPOSED)
881
        team1_on_team2.setStatus(TeamMembershipStatus.DECLINED, self.foobar)
882
        self.assertEqual(team1_on_team2.status, TeamMembershipStatus.DECLINED)
883
        team1_on_team2.setStatus(TeamMembershipStatus.ADMIN, self.foobar)
884
        self.assertEqual(team1_on_team2.status, TeamMembershipStatus.ADMIN)
6220.2.2 by Guilherme Salgado
Fix to prevent cyclical participation as well.
885
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
886
    def test_invited_member_can_be_declined(self):
11818.6.11 by Curtis Hovey
Revised documentation.
887
        # A team can decline an invited member.
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
888
        self.team2.addMember(self.team1, self.no_priv)
889
        tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
890
            self.team1, self.team2)
891
        tm.setStatus(
892
            TeamMembershipStatus.INVITATION_DECLINED, self.team2.teamowner)
893
        self.assertEqual(TeamMembershipStatus.INVITATION_DECLINED, tm.status)
894
13785.7.1 by Aaron Bentley
Allow inviting previously-declined members.
895
    def test_declined_member_can_be_invited(self):
896
        # A team can re-invite a declined member.
897
        self.team2.addMember(
898
            self.team1, self.no_priv, status=TeamMembershipStatus.PROPOSED,
899
            force_team_add=True)
900
        tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
901
            self.team1, self.team2)
902
        tm.setStatus(
903
            TeamMembershipStatus.DECLINED, self.team1.teamowner)
904
        tm.setStatus(
905
            TeamMembershipStatus.INVITED, self.team1.teamowner)
906
        self.assertEqual(TeamMembershipStatus.INVITED, tm.status)
907
13785.7.2 by Aaron Bentley
Fix several team membership bugs.
908
    def test_add_approved(self):
909
        # Adding an approved team is a no-op.
910
        member_team = self.factory.makeTeam()
911
        self.team1.addMember(
912
            member_team, self.team1.teamowner)
913
        with person_logged_in(member_team.teamowner):
914
            member_team.acceptInvitationToBeMemberOf(self.team1, 'alright')
915
        self.team1.addMember(
916
            member_team, self.team1.teamowner)
917
        tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
918
            member_team, self.team1)
919
        self.assertEqual(TeamMembershipStatus.APPROVED, tm.status)
920
        self.team1.addMember(
921
            member_team, member_team.teamowner)
922
        self.assertEqual(TeamMembershipStatus.APPROVED, tm.status)
923
924
    def test_add_admin(self):
925
        # Adding an admin team is a no-op.
926
        member_team = self.factory.makeTeam()
927
        self.team1.addMember(
928
            member_team, self.team1.teamowner,
929
            status=TeamMembershipStatus.ADMIN, force_team_add=True)
930
        self.team1.addMember(
931
            member_team, self.team1.teamowner)
932
        tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
933
            member_team, self.team1)
934
        self.assertEqual(TeamMembershipStatus.ADMIN, tm.status)
935
        self.team1.addMember(
936
            member_team, member_team.teamowner)
937
        self.assertEqual(TeamMembershipStatus.ADMIN, tm.status)
938
939
    def test_implicit_approval(self):
940
        # Inviting a proposed person is an implicit approval.
941
        member_team = self.factory.makeTeam()
942
        self.team1.addMember(
943
            member_team, self.team1.teamowner,
944
            status=TeamMembershipStatus.PROPOSED, force_team_add=True)
945
        self.team1.addMember(
946
            member_team, self.team1.teamowner)
947
        tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
948
            member_team, self.team1)
949
        self.assertEqual(TeamMembershipStatus.APPROVED, tm.status)
950
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
951
    def test_retractTeamMembership_invited(self):
11818.6.11 by Curtis Hovey
Revised documentation.
952
        # A team can retract a membership invitation.
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
953
        self.team2.addMember(self.team1, self.no_priv)
954
        self.team1.retractTeamMembership(self.team2, self.team1.teamowner)
955
        tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
956
            self.team1, self.team2)
957
        self.assertEqual(TeamMembershipStatus.INVITATION_DECLINED, tm.status)
958
959
    def test_retractTeamMembership_proposed(self):
11818.6.11 by Curtis Hovey
Revised documentation.
960
        # A team can retract the proposed membership in a team.
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
961
        self.team2.subscriptionpolicy = TeamSubscriptionPolicy.MODERATED
962
        self.team1.join(self.team2, self.team1.teamowner)
963
        self.team1.retractTeamMembership(self.team2, self.team1.teamowner)
964
        tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
965
            self.team1, self.team2)
966
        self.assertEqual(TeamMembershipStatus.DECLINED, tm.status)
967
968
    def test_retractTeamMembership_active(self):
11818.6.11 by Curtis Hovey
Revised documentation.
969
        # A team can retract the membership in a team.
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
970
        self.team1.join(self.team2, self.team1.teamowner)
971
        self.team1.retractTeamMembership(self.team2, self.team1.teamowner)
972
        tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
973
            self.team1, self.team2)
974
        self.assertEqual(TeamMembershipStatus.DEACTIVATED, tm.status)
975
976
    def test_retractTeamMembership_admin(self):
11818.6.11 by Curtis Hovey
Revised documentation.
977
        # A team can retract the membership in a team.
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
978
        self.team1.join(self.team2, self.team1.teamowner)
979
        tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
980
            self.team1, self.team2)
981
        tm.setStatus(TeamMembershipStatus.ADMIN, self.team2.teamowner)
982
        self.team1.retractTeamMembership(self.team2, self.team1.teamowner)
11818.6.2 by Curtis Hovey
Added retractTeamMembership so that teams can leave super teams. Allow users to delete teams with user teams.
983
        self.assertEqual(TeamMembershipStatus.DEACTIVATED, tm.status)
984
5825.2.3 by Guilherme Salgado
Fix the damn thing
985
12481.1.1 by Curtis Hovey
Added a simple unittest to verify the behavioursendExpirationWarningEmail().
986
class TestTeamMembershipSendExpirationWarningEmail(TestCaseWithFactory):
987
    """Test the behaviour of sendExpirationWarningEmail()."""
988
    layer = DatabaseFunctionalLayer
989
990
    def setUp(self):
991
        super(TestTeamMembershipSendExpirationWarningEmail, self).setUp()
12481.1.3 by Curtis Hovey
Use names for the team and member to simplify tests.
992
        self.member = self.factory.makePerson(name='green')
993
        self.team = self.factory.makeTeam(name='red')
12481.1.1 by Curtis Hovey
Added a simple unittest to verify the behavioursendExpirationWarningEmail().
994
        login_person(self.team.teamowner)
995
        self.team.addMember(self.member, self.team.teamowner)
996
        self.tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
997
            self.member, self.team)
998
        pop_notifications()
999
12481.1.4 by Curtis Hovey
Added the person and team to the assertion messag.
1000
    def test_error_raised_when_no_expiration(self):
1001
        # An exception is raised if the membership does not have an
1002
        # expiration date.
1003
        self.assertEqual(None, self.tm.dateexpires)
12481.1.5 by Curtis Hovey
Updated sendExpirationWarningEmail documentation. Do not assert that the expiration date has past; silently return instead.
1004
        message = 'green in team red has no membership expiration date.'
1005
        self.assertRaisesWithContent(
1006
            AssertionError, message, self.tm.sendExpirationWarningEmail)
1007
1008
    def test_error_raised_for_team_with_automatic_renewal(self):
1009
        # An exception is raised if the team's TeamMembershipRenewalPolicy
1010
        # is AUTOMATIC.
1011
        self.team.renewal_policy = TeamMembershipRenewalPolicy.AUTOMATIC
1012
        self.team.defaultrenewalperiod = 365
1013
        tomorrow = datetime.now(pytz.UTC) + timedelta(days=1)
1014
        removeSecurityProxy(self.tm).dateexpires = tomorrow
1015
        message = (
1016
            'Team red with automatic renewals should not send '
1017
            'expiration warnings.')
12481.1.4 by Curtis Hovey
Added the person and team to the assertion messag.
1018
        self.assertRaisesWithContent(
1019
            AssertionError, message, self.tm.sendExpirationWarningEmail)
1020
12481.1.1 by Curtis Hovey
Added a simple unittest to verify the behavioursendExpirationWarningEmail().
1021
    def test_message_sent_for_future_expiration(self):
1022
        # An email is sent to the user whose membership will expire.
12481.1.5 by Curtis Hovey
Updated sendExpirationWarningEmail documentation. Do not assert that the expiration date has past; silently return instead.
1023
        tomorrow = datetime.now(pytz.UTC) + timedelta(days=1)
1024
        removeSecurityProxy(self.tm).dateexpires = tomorrow
12481.1.1 by Curtis Hovey
Added a simple unittest to verify the behavioursendExpirationWarningEmail().
1025
        self.tm.sendExpirationWarningEmail()
1026
        notifications = pop_notifications()
1027
        self.assertEqual(1, len(notifications))
1028
        message = notifications[0]
1029
        self.assertEqual(
12481.1.3 by Curtis Hovey
Use names for the team and member to simplify tests.
1030
            'Your membership in red is about to expire', message['subject'])
12481.1.1 by Curtis Hovey
Added a simple unittest to verify the behavioursendExpirationWarningEmail().
1031
        self.assertEqual(
12481.1.3 by Curtis Hovey
Use names for the team and member to simplify tests.
1032
            self.member.preferredemail.email, message['to'])
12481.1.1 by Curtis Hovey
Added a simple unittest to verify the behavioursendExpirationWarningEmail().
1033
12481.1.5 by Curtis Hovey
Updated sendExpirationWarningEmail documentation. Do not assert that the expiration date has past; silently return instead.
1034
    def test_no_message_sent_for_expired_memberships(self):
1035
        # Members whose membership has expired do not get a message.
1036
        yesterday = datetime.now(pytz.UTC) - timedelta(days=1)
1037
        removeSecurityProxy(self.tm).dateexpires = yesterday
1038
        self.tm.sendExpirationWarningEmail()
1039
        notifications = pop_notifications()
1040
        self.assertEqual(0, len(notifications))
1041
12481.1.1 by Curtis Hovey
Added a simple unittest to verify the behavioursendExpirationWarningEmail().
1042
    def test_no_message_sent_for_non_active_users(self):
1043
        # Non-active users do not get an expiration message.
1044
        with person_logged_in(self.member):
1045
            self.member.deactivateAccount('Goodbye.')
1046
        IStore(self.member).flush()
1047
        now = datetime.now(pytz.UTC)
1048
        removeSecurityProxy(self.tm).dateexpires = now + timedelta(days=1)
1049
        self.tm.sendExpirationWarningEmail()
1050
        notifications = pop_notifications()
1051
        self.assertEqual(0, len(notifications))
1052
1053
14213.4.3 by Gavin Panella
New test case TestCheckTeamParticipationScriptPerformance that runs check_teamparticipation() in-process, so we can monitor database activity more closely.
1054
class TestCheckTeamParticipationScript(TestCase):
14213.4.1 by Gavin Panella
Beginnings of performance optimization work for check-teamparticipation.py.
1055
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
1056
    layer = DatabaseFunctionalLayer
4868.1.6 by Guilherme Salgado
Turn the script that was supposed to fix invalid team participation entries into something that just reports the invalid ones, to be run from staging
1057
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
1058
    def _runScript(self, *args):
1059
        cmd = ["cronscripts/check-teamparticipation.py"]
1060
        cmd.extend(args)
4868.1.6 by Guilherme Salgado
Turn the script that was supposed to fix invalid team participation entries into something that just reports the invalid ones, to be run from staging
1061
        process = subprocess.Popen(
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
1062
            cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
4868.1.6 by Guilherme Salgado
Turn the script that was supposed to fix invalid team participation entries into something that just reports the invalid ones, to be run from staging
1063
            stderr=subprocess.PIPE)
14213.4.5 by Gavin Panella
Improve the tests to add the logs as details.
1064
        out, err = process.communicate()
14213.4.17 by Gavin Panella
Use inequality instead of len().
1065
        if out != "":
14213.4.5 by Gavin Panella
Improve the tests to add the logs as details.
1066
            self.addDetail("stdout", text_content(out))
14213.4.17 by Gavin Panella
Use inequality instead of len().
1067
        if err != "":
14213.4.5 by Gavin Panella
Improve the tests to add the logs as details.
1068
            self.addDetail("stderr", text_content(err))
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
1069
        return process.poll(), out, err
4868.1.6 by Guilherme Salgado
Turn the script that was supposed to fix invalid team participation entries into something that just reports the invalid ones, to be run from staging
1070
1071
    def test_no_output_if_no_invalid_entries(self):
1072
        """No output if there's no invalid teamparticipation entries."""
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
1073
        code, out, err = self._runScript()
1074
        self.assertEqual(0, code)
14213.4.5 by Gavin Panella
Improve the tests to add the logs as details.
1075
        self.assertEqual(0, len(out))
1076
        self.assertEqual(0, len(err))
4868.1.6 by Guilherme Salgado
Turn the script that was supposed to fix invalid team participation entries into something that just reports the invalid ones, to be run from staging
1077
1078
    def test_report_invalid_teamparticipation_entries(self):
7052.1.2 by Guilherme Salgado
Fix the bug.
1079
        """The script reports missing/spurious TeamParticipation entries.
6010.1.1 by Guilherme Salgado
Change the check-teamparticipation.py script to check for people/teams which are not members of themselves.
1080
1081
        As well as missing self-participation.
1082
        """
4868.1.6 by Guilherme Salgado
Turn the script that was supposed to fix invalid team participation entries into something that just reports the invalid ones, to be run from staging
1083
        cur = cursor()
7052.1.2 by Guilherme Salgado
Fix the bug.
1084
        # Create a new entry in the Person table and change its
1085
        # self-participation entry, making that person a participant in a team
1086
        # where it should not be as well as making that person not a member of
1087
        # itself (as everybody should be).
4868.1.6 by Guilherme Salgado
Turn the script that was supposed to fix invalid team participation entries into something that just reports the invalid ones, to be run from staging
1088
        cur.execute("""
6010.1.1 by Guilherme Salgado
Change the check-teamparticipation.py script to check for people/teams which are not members of themselves.
1089
            INSERT INTO
6198.1.30 by Stuart Bishop
Fix teammembership tests
1090
                Person (id, name, displayname, creation_rationale)
1091
                VALUES (9999, 'zzzzz', 'zzzzzz', 1);
6010.1.2 by Guilherme Salgado
Fix a few stylistic things.
1092
            UPDATE TeamParticipation
6010.1.1 by Guilherme Salgado
Change the check-teamparticipation.py script to check for people/teams which are not members of themselves.
1093
                SET team = (
7052.1.2 by Guilherme Salgado
Fix the bug.
1094
                    SELECT id
1095
                    FROM Person
1096
                    WHERE teamowner IS NOT NULL
1097
                    ORDER BY name
1098
                    LIMIT 1)
6010.1.1 by Guilherme Salgado
Change the check-teamparticipation.py script to check for people/teams which are not members of themselves.
1099
                WHERE person = 9999;
4868.1.6 by Guilherme Salgado
Turn the script that was supposed to fix invalid team participation entries into something that just reports the invalid ones, to be run from staging
1100
            """)
7052.1.2 by Guilherme Salgado
Fix the bug.
1101
        # Now add the new person as a member of another team but don't create
1102
        # the relevant TeamParticipation for that person on that team.
1103
        cur.execute("""
1104
            INSERT INTO
1105
                TeamMembership (person, team, status)
1106
                VALUES (9999,
1107
                    (SELECT id
1108
                        FROM Person
1109
                        WHERE teamowner IS NOT NULL
1110
                        ORDER BY name desc
1111
                        LIMIT 1),
1112
                    %s);
1113
            """ % sqlvalues(TeamMembershipStatus.APPROVED))
4868.1.6 by Guilherme Salgado
Turn the script that was supposed to fix invalid team participation entries into something that just reports the invalid ones, to be run from staging
1114
        transaction.commit()
6010.1.1 by Guilherme Salgado
Change the check-teamparticipation.py script to check for people/teams which are not members of themselves.
1115
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
1116
        code, out, err = self._runScript()
1117
        self.assertEqual(0, code)
14213.4.5 by Gavin Panella
Improve the tests to add the logs as details.
1118
        self.assertEqual(0, len(out))
1119
        self.failUnless(
1120
            re.search('missing TeamParticipation entries for zzzzz', err))
1121
        self.failUnless(
1122
            re.search('spurious TeamParticipation entries for zzzzz', err))
4868.1.6 by Guilherme Salgado
Turn the script that was supposed to fix invalid team participation entries into something that just reports the invalid ones, to be run from staging
1123
7052.1.3 by Guilherme Salgado
Make check-teamparticipation.py detect circular references between teams.
1124
    def test_report_circular_team_references(self):
1125
        """The script reports circular references between teams.
1126
1127
        If that happens, though, the script will have to report the circular
1128
        references and exit, to avoid an infinite loop when checking for
1129
        missing/spurious TeamParticipation entries.
1130
        """
1131
        # Create two new teams and make them members of each other.
1132
        cursor().execute("""
1133
            INSERT INTO
1134
                Person (id, name, displayname, teamowner)
1135
                VALUES (9998, 'test-team1', 'team1', 1);
1136
            INSERT INTO
1137
                Person (id, name, displayname, teamowner)
1138
                VALUES (9997, 'test-team2', 'team2', 1);
1139
            INSERT INTO
1140
                TeamMembership (person, team, status)
1141
                VALUES (9998, 9997, %(approved)s);
1142
            INSERT INTO
1143
                TeamParticipation (person, team)
1144
                VALUES (9998, 9997);
1145
            INSERT INTO
1146
                TeamMembership (person, team, status)
1147
                VALUES (9997, 9998, %(approved)s);
1148
            INSERT INTO
1149
                TeamParticipation (person, team)
1150
                VALUES (9997, 9998);
1151
            """ % sqlvalues(approved=TeamMembershipStatus.APPROVED))
1152
        transaction.commit()
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
1153
        code, out, err = self._runScript()
1154
        self.assertEqual(1, code)
14213.4.5 by Gavin Panella
Improve the tests to add the logs as details.
1155
        self.assertEqual(0, len(out))
1156
        self.failUnless(re.search('Circular references found', err))
7052.1.3 by Guilherme Salgado
Make check-teamparticipation.py detect circular references between teams.
1157
14464.1.2 by Gavin Panella
Test for new function fix_teamparticipation_consistency().
1158
    # A script to create two new people, where both participate in the first,
1159
    # and first is missing a self-participation.
1160
    script_create_inconsistent_participation = """
1161
        INSERT INTO
1162
            Person (id, name, displayname, creation_rationale)
1163
            VALUES (6969, 'bobby', 'Dazzler', 1);
1164
        INSERT INTO
1165
            Person (id, name, displayname, creation_rationale)
1166
            VALUES (6970, 'nobby', 'Jazzler', 1);
1167
        INSERT INTO
1168
            TeamParticipation (person, team)
1169
            VALUES (6970, 6969);
1170
        DELETE FROM
1171
            TeamParticipation
1172
            WHERE person = 6969
1173
              AND team = 6969;
14464.1.5 by Gavin Panella
Commit changes so that other connections can see them.
1174
        """
14464.1.2 by Gavin Panella
Test for new function fix_teamparticipation_consistency().
1175
14464.1.3 by Gavin Panella
Rename test_report_spurious_participants_of_people to test_check_teamparticipation_consistency.
1176
    def test_check_teamparticipation_consistency(self):
14213.4.19 by Gavin Panella
Check that the only participant of a person is the person.
1177
        """The script reports spurious participants of people.
1178
1179
        Teams can have multiple participants, but only the person should be a
1180
        paricipant of him/herself.
1181
        """
14464.1.2 by Gavin Panella
Test for new function fix_teamparticipation_consistency().
1182
        cursor().execute(self.script_create_inconsistent_participation)
14464.1.5 by Gavin Panella
Commit changes so that other connections can see them.
1183
        transaction.commit()
14213.4.19 by Gavin Panella
Check that the only participant of a person is the person.
1184
        logger = BufferLogger()
14213.4.31 by Gavin Panella
Use BufferLogger.content in tests.
1185
        self.addDetail("log", logger.content)
14213.4.37 by Gavin Panella
Decouple check_teamparticipation_consistency() from fetch_team_participation_info().
1186
        errors = check_teamparticipation_consistency(
1187
            logger, fetch_team_participation_info(logger))
14464.1.1 by Gavin Panella
Demonstrate check_teamparticipation_consistency() finding a missing TeamParticipation.
1188
        errors_expected = [
1189
            ConsistencyError("spurious", 6969, [6970]),
1190
            ConsistencyError("missing", 6969, [6969]),
1191
            ]
1192
        self.assertContentEqual(errors_expected, errors)
14213.4.19 by Gavin Panella
Check that the only participant of a person is the person.
1193
14464.1.2 by Gavin Panella
Test for new function fix_teamparticipation_consistency().
1194
    def test_fix_teamparticipation_consistency(self):
1195
        """
1196
        `fix_teamparticipation_consistency` takes an iterable of
1197
        `ConsistencyError`s and attempts to repair the data.
1198
        """
1199
        cursor().execute(self.script_create_inconsistent_participation)
14464.1.5 by Gavin Panella
Commit changes so that other connections can see them.
1200
        transaction.commit()
14464.1.2 by Gavin Panella
Test for new function fix_teamparticipation_consistency().
1201
        logger = BufferLogger()
1202
        self.addDetail("log", logger.content)
1203
        errors = check_teamparticipation_consistency(
1204
            logger, fetch_team_participation_info(logger))
1205
        self.assertNotEqual([], errors)
1206
        fix_teamparticipation_consistency(logger, errors)
1207
        errors = check_teamparticipation_consistency(
1208
            logger, fetch_team_participation_info(logger))
1209
        self.assertEqual([], errors)
1210
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
1211
    def test_load_and_save_team_participation(self):
14213.4.28 by Gavin Panella
Update docstring.
1212
        """The script can load and save participation info."""
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
1213
        logger = BufferLogger()
14213.4.31 by Gavin Panella
Use BufferLogger.content in tests.
1214
        self.addDetail("log", logger.content)
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
1215
        info = fetch_team_participation_info(logger)
1216
        tempdir = self.useFixture(TempDir()).path
1217
        filename_in = os.path.join(tempdir, "info.in")
1218
        filename_out = os.path.join(tempdir, "info.out")
1219
        fout = bz2.BZ2File(filename_in, "w")
1220
        try:
1221
            pickle.dump(info, fout, pickle.HIGHEST_PROTOCOL)
1222
        finally:
1223
            fout.close()
1224
        code, out, err = self._runScript(
1225
            "--load-participation-info", filename_in,
1226
            "--save-participation-info", filename_out)
1227
        self.assertEqual(0, code)
1228
        fin = bz2.BZ2File(filename_out, "r")
1229
        try:
1230
            saved_info = pickle.load(fin)
1231
        finally:
1232
            fin.close()
1233
        self.assertEqual(info, saved_info)
1234
14213.4.3 by Gavin Panella
New test case TestCheckTeamParticipationScriptPerformance that runs check_teamparticipation() in-process, so we can monitor database activity more closely.
1235
1236
class TestCheckTeamParticipationScriptPerformance(TestCaseWithFactory):
1237
14213.4.13 by Gavin Panella
Demonstrate that a constant number of queries are issued for the whole check_teamparticipation() run.
1238
    layer = DatabaseFunctionalLayer
14213.4.3 by Gavin Panella
New test case TestCheckTeamParticipationScriptPerformance that runs check_teamparticipation() in-process, so we can monitor database activity more closely.
1239
14213.4.1 by Gavin Panella
Beginnings of performance optimization work for check-teamparticipation.py.
1240
    def test_queries(self):
14213.4.13 by Gavin Panella
Demonstrate that a constant number of queries are issued for the whole check_teamparticipation() run.
1241
        """The script does not overly tax the database.
1242
1243
        The whole check_teamparticipation() run executes a constant low number
1244
        of queries.
1245
        """
14213.4.1 by Gavin Panella
Beginnings of performance optimization work for check-teamparticipation.py.
1246
        # Create a deeply nested team and member structure.
1247
        team = self.factory.makeTeam()
1248
        for num in xrange(10):
1249
            another_team = self.factory.makeTeam()
1250
            another_person = self.factory.makePerson()
1251
            with person_logged_in(team.teamowner):
1252
                team.addMember(another_team, team.teamowner)
1253
                team.addMember(another_person, team.teamowner)
1254
            team = another_team
14213.4.13 by Gavin Panella
Demonstrate that a constant number of queries are issued for the whole check_teamparticipation() run.
1255
        transaction.commit()
14213.4.31 by Gavin Panella
Use BufferLogger.content in tests.
1256
        logger = BufferLogger()
1257
        self.addDetail("log", logger.content)
14213.4.13 by Gavin Panella
Demonstrate that a constant number of queries are issued for the whole check_teamparticipation() run.
1258
        with StormStatementRecorder() as recorder:
14213.4.26 by Gavin Panella
Enable loading and saving of team participation info. Work in progress.
1259
            check_teamparticipation_circular(logger)
14213.4.37 by Gavin Panella
Decouple check_teamparticipation_consistency() from fetch_team_participation_info().
1260
            check_teamparticipation_consistency(
1261
                logger, fetch_team_participation_info(logger))
14464.2.1 by Gavin Panella
Remove check_teamparticipation_self() because this is covered by check_teamparticipation_consistency().
1262
        self.assertThat(recorder, HasQueryCount(Equals(5)))
14213.4.1 by Gavin Panella
Beginnings of performance optimization work for check-teamparticipation.py.
1263
4868.1.6 by Guilherme Salgado
Turn the script that was supposed to fix invalid team participation entries into something that just reports the invalid ones, to be run from staging
1264
3691.272.16 by Guilherme Salgado
A bunch of fixes and new tests suggested by Bjorn on his review
1265
def test_suite():
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
1266
    suite = TestLoader().loadTestsFromName(__name__)
6702.1.2 by Guilherme Salgado
A couple changes suggested by Francis
1267
    bug_249185 = LayeredDocFileSuite(
1268
        'bug-249185.txt', optionflags=default_optionflags,
11818.6.7 by Curtis Hovey
Added test to verify corner-cases with managing super teams are handled.
1269
        layer=DatabaseFunctionalLayer, setUp=setUp, tearDown=tearDown)
6702.1.2 by Guilherme Salgado
A couple changes suggested by Francis
1270
    suite.addTest(bug_249185)
1271
    return suite