1 # Copyright (C) 2015-2022 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <https://www.gnu.org/licenses/>.
18 """Test the `mailman members` command."""
22 from click
.testing
import CliRunner
23 from mailman
.app
.lifecycle
import create_list
24 from mailman
.commands
.cli_members
import members
25 from mailman
.interfaces
.member
import MemberRole
26 from mailman
.testing
.helpers
import subscribe
27 from mailman
.testing
.layers
import ConfigLayer
28 from tempfile
import NamedTemporaryFile
31 class TestCLIMembers(unittest
.TestCase
):
35 self
._mlist
= create_list('ant@example.com')
36 self
._command
= CliRunner()
38 def test_no_such_list(self
):
39 result
= self
._command
.invoke(members
, ('bee.example.com',))
40 self
.assertEqual(result
.exit_code
, 2)
43 'Usage: members [OPTIONS] LISTSPEC\n'
44 'Try \'members --help\' for help.\n\n'
45 'Error: No such list: bee.example.com\n')
47 def test_role_administrator(self
):
48 subscribe(self
._mlist
, 'Anne', role
=MemberRole
.owner
)
49 subscribe(self
._mlist
, 'Bart', role
=MemberRole
.moderator
)
50 subscribe(self
._mlist
, 'Cate', role
=MemberRole
.nonmember
)
51 subscribe(self
._mlist
, 'Dave', role
=MemberRole
.member
)
52 with
NamedTemporaryFile('w', encoding
='utf-8') as outfp
:
53 self
._command
.invoke(members
, (
54 '--role', 'administrator', '-o', outfp
.name
,
56 with
open(outfp
.name
, 'r', encoding
='utf-8') as infp
:
57 lines
= infp
.readlines()
58 self
.assertEqual(len(lines
), 2)
59 self
.assertEqual(lines
[0], 'Anne Person <aperson@example.com>\n')
60 self
.assertEqual(lines
[1], 'Bart Person <bperson@example.com>\n')
62 def test_role_any(self
):
63 subscribe(self
._mlist
, 'Anne', role
=MemberRole
.owner
)
64 subscribe(self
._mlist
, 'Bart', role
=MemberRole
.moderator
)
65 subscribe(self
._mlist
, 'Cate', role
=MemberRole
.nonmember
)
66 subscribe(self
._mlist
, 'Dave', role
=MemberRole
.member
)
67 with
NamedTemporaryFile('w', encoding
='utf-8') as outfp
:
68 self
._command
.invoke(members
, (
69 '--role', 'any', '-o', outfp
.name
, 'ant.example.com'))
70 with
open(outfp
.name
, 'r', encoding
='utf-8') as infp
:
71 lines
= infp
.readlines()
72 self
.assertEqual(len(lines
), 4)
73 self
.assertEqual(lines
[0], 'Anne Person <aperson@example.com>\n')
74 self
.assertEqual(lines
[1], 'Bart Person <bperson@example.com>\n')
75 self
.assertEqual(lines
[2], 'Cate Person <cperson@example.com>\n')
76 self
.assertEqual(lines
[3], 'Dave Person <dperson@example.com>\n')
78 def test_role_moderator(self
):
79 subscribe(self
._mlist
, 'Anne', role
=MemberRole
.owner
)
80 subscribe(self
._mlist
, 'Bart', role
=MemberRole
.moderator
)
81 subscribe(self
._mlist
, 'Cate', role
=MemberRole
.nonmember
)
82 subscribe(self
._mlist
, 'Dave', role
=MemberRole
.member
)
83 with
NamedTemporaryFile('w', encoding
='utf-8') as outfp
:
84 self
._command
.invoke(members
, (
85 '--role', 'moderator', '-o', outfp
.name
, 'ant.example.com'))
86 with
open(outfp
.name
, 'r', encoding
='utf-8') as infp
:
87 lines
= infp
.readlines()
88 self
.assertEqual(len(lines
), 1)
89 self
.assertEqual(lines
[0], 'Bart Person <bperson@example.com>\n')
91 def test_role_nonmember(self
):
92 subscribe(self
._mlist
, 'Anne', role
=MemberRole
.owner
)
93 subscribe(self
._mlist
, 'Bart', role
=MemberRole
.moderator
)
94 subscribe(self
._mlist
, 'Cate', role
=MemberRole
.nonmember
)
95 subscribe(self
._mlist
, 'Dave', role
=MemberRole
.member
)
96 with
NamedTemporaryFile('w', encoding
='utf-8') as outfp
:
97 self
._command
.invoke(members
, (
98 '--role', 'nonmember', '-o', outfp
.name
, 'ant.example.com'))
99 with
open(outfp
.name
, 'r', encoding
='utf-8') as infp
:
100 lines
= infp
.readlines()
101 self
.assertEqual(len(lines
), 1)
102 self
.assertEqual(lines
[0], 'Cate Person <cperson@example.com>\n')
104 def test_already_subscribed_with_display_name(self
):
105 subscribe(self
._mlist
, 'Anne')
106 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
107 print('Anne Person <aperson@example.com>', file=infp
)
108 result
= self
._command
.invoke(members
, (
109 '--add', infp
.name
, 'ant.example.com'))
112 'Usage: members [OPTIONS] LISTSPEC\n'
113 'Try \'members --help\' for help.\n\n'
114 'Error: The --add option is removed. Use '
115 '`mailman addmembers` instead.\n')
117 def test_add_invalid_email(self
):
118 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
119 print('foobar@', file=infp
)
120 result
= self
._command
.invoke(members
, (
121 '--add', infp
.name
, 'ant.example.com'))
124 'Usage: members [OPTIONS] LISTSPEC\n'
125 'Try \'members --help\' for help.\n\n'
126 'Error: The --add option is removed. Use '
127 '`mailman addmembers` instead.\n')
129 def test_not_subscribed_without_display_name(self
):
130 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
131 print('aperson@example.com', file=infp
)
132 result
= self
._command
.invoke(members
, (
133 '--delete', infp
.name
, 'ant.example.com'))
136 'Usage: members [OPTIONS] LISTSPEC\n'
137 'Try \'members --help\' for help.\n\n'
138 'Error: The --delete option is removed. Use '
139 '`mailman delmembers` instead.\n')
141 def test_not_subscribed_with_display_name(self
):
142 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
143 print('Anne Person <aperson@example.com>', file=infp
)
144 result
= self
._command
.invoke(members
, (
145 '--delete', infp
.name
, 'ant.example.com'))
148 'Usage: members [OPTIONS] LISTSPEC\n'
149 'Try \'members --help\' for help.\n\n'
150 'Error: The --delete option is removed. Use '
151 '`mailman delmembers` instead.\n')
153 def test_deletion_blank_lines(self
):
154 subscribe(self
._mlist
, 'Anne')
155 subscribe(self
._mlist
, 'Bart')
156 subscribe(self
._mlist
, 'Cate')
157 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
158 print('Anne Person <aperson@example.com>', file=infp
)
160 print(' ', file=infp
)
161 print('\t', file=infp
)
162 print('Bart Person <bperson@example.com>', file=infp
)
163 result
= self
._command
.invoke(members
, (
164 '--delete', infp
.name
, 'ant.example.com'))
167 'Usage: members [OPTIONS] LISTSPEC\n'
168 'Try \'members --help\' for help.\n\n'
169 'Error: The --delete option is removed. Use '
170 '`mailman delmembers` instead.\n')
171 with
NamedTemporaryFile('w', encoding
='utf-8') as outfp
:
172 self
._command
.invoke(members
, (
173 '-o', outfp
.name
, 'ant.example.com'))
174 with
open(outfp
.name
, 'r', encoding
='utf-8') as infp
:
175 lines
= infp
.readlines()
176 self
.assertEqual(len(lines
), 3)
177 self
.assertEqual(lines
[0], 'Anne Person <aperson@example.com>\n')
179 def test_deletion_commented_lines(self
):
180 subscribe(self
._mlist
, 'Anne')
181 subscribe(self
._mlist
, 'Bart')
182 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
183 print('Anne Person <aperson@example.com>', file=infp
)
184 print('#Bart Person <bperson@example.com>', file=infp
)
185 result
= self
._command
.invoke(members
, (
186 '--delete', infp
.name
, 'ant.example.com'))
189 'Usage: members [OPTIONS] LISTSPEC\n'
190 'Try \'members --help\' for help.\n\n'
191 'Error: The --delete option is removed. Use '
192 '`mailman delmembers` instead.\n')
194 with
NamedTemporaryFile('w', encoding
='utf-8') as outfp
:
195 self
._command
.invoke(members
, (
196 '-o', outfp
.name
, 'ant.example.com'))
197 with
open(outfp
.name
, 'r', encoding
='utf-8') as infp
:
198 lines
= infp
.readlines()
199 self
.assertEqual(len(lines
), 2)
200 self
.assertEqual(lines
[0], 'Anne Person <aperson@example.com>\n')
202 def test_sync_invalid_email(self
):
203 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
204 print('Dont Subscribe <not-a-valid-email>', file=infp
)
205 print('not-a-valid@email', file=infp
)
206 result
= self
._command
.invoke(members
, (
207 '--sync', infp
.name
, 'ant.example.com'))
210 'Usage: members [OPTIONS] LISTSPEC\n'
211 'Try \'members --help\' for help.\n\n'
212 'Error: The --sync option is removed. '
213 'Use `mailman syncmembers` instead.\n')
215 def test_sync_no_change(self
):
216 subscribe(self
._mlist
, 'Anne')
217 subscribe(self
._mlist
, 'Bart')
218 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
219 print('Anne Person <aperson@example.com>', file=infp
)
220 result
= self
._command
.invoke(members
, (
221 '--no-change', '--sync', infp
.name
, 'ant.example.com'))
224 'Usage: members [OPTIONS] LISTSPEC\n'
225 'Try \'members --help\' for help.\n\n'
226 'Error: The --sync option is removed. '
227 'Use `mailman syncmembers` instead.\n')
229 def test_sync_empty_tuple(self
):
230 subscribe(self
._mlist
, 'Anne')
231 subscribe(self
._mlist
, 'Bart')
232 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
233 print('Anne Person <aperson@example.com>', file=infp
)
234 print('\"\"', file=infp
)
235 result
= self
._command
.invoke(members
, (
236 '--no-change', '--sync', infp
.name
, 'ant.example.com'))
239 'Usage: members [OPTIONS] LISTSPEC\n'
240 'Try \'members --help\' for help.\n\n'
241 'Error: The --sync option is removed. '
242 'Use `mailman syncmembers` instead.\n')
244 def test_sync_commented_lines(self
):
245 subscribe(self
._mlist
, 'Anne')
246 subscribe(self
._mlist
, 'Bart')
247 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
248 print('Anne Person <aperson@example.com>', file=infp
)
249 print('#Bart Person <bperson@example.com>', file=infp
)
250 result
= self
._command
.invoke(members
, (
251 '--sync', infp
.name
, 'ant.example.com'))
254 'Usage: members [OPTIONS] LISTSPEC\n'
255 'Try \'members --help\' for help.\n\n'
256 'Error: The --sync option is removed. '
257 'Use `mailman syncmembers` instead.\n')
259 with
NamedTemporaryFile('w', encoding
='utf-8') as outfp
:
260 self
._command
.invoke(members
, (
261 '-o', outfp
.name
, 'ant.example.com'))
262 with
open(outfp
.name
, 'r', encoding
='utf-8') as infp
:
263 lines
= infp
.readlines()
264 self
.assertEqual(len(lines
), 2)
265 self
.assertEqual(lines
[0], 'Anne Person <aperson@example.com>\n')
267 def test_sync_blank_lines(self
):
268 subscribe(self
._mlist
, 'Anne')
269 subscribe(self
._mlist
, 'Bart')
270 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
271 print('Anne Person <aperson@example.com>', file=infp
)
274 result
= self
._command
.invoke(members
, (
275 '--sync', infp
.name
, 'ant.example.com'))
278 'Usage: members [OPTIONS] LISTSPEC\n'
279 'Try \'members --help\' for help.\n\n'
280 'Error: The --sync option is removed. '
281 'Use `mailman syncmembers` instead.\n')
283 with
NamedTemporaryFile('w', encoding
='utf-8') as outfp
:
284 self
._command
.invoke(members
, (
285 '-o', outfp
.name
, 'ant.example.com'))
286 with
open(outfp
.name
, 'r', encoding
='utf-8') as infp
:
287 lines
= infp
.readlines()
288 self
.assertEqual(len(lines
), 2)
289 self
.assertEqual(lines
[0], 'Anne Person <aperson@example.com>\n')
291 def test_sync_nothing_to_do(self
):
292 subscribe(self
._mlist
, 'Anne')
293 subscribe(self
._mlist
, 'Bart')
294 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
295 print('Anne Person <aperson@example.com>', file=infp
)
296 print('Bart Person <bperson@example.com>', file=infp
)
297 result
= self
._command
.invoke(members
, (
298 '--sync', infp
.name
, 'ant.example.com'))
301 'Usage: members [OPTIONS] LISTSPEC\n'
302 'Try \'members --help\' for help.\n\n'
303 'Error: The --sync option is removed. '
304 'Use `mailman syncmembers` instead.\n')
306 with
NamedTemporaryFile('w', encoding
='utf-8') as outfp
:
307 self
._command
.invoke(members
, (
308 '-o', outfp
.name
, 'ant.example.com'))
309 with
open(outfp
.name
, 'r', encoding
='utf-8') as infp
:
310 lines
= infp
.readlines()
311 self
.assertEqual(len(lines
), 2)
312 self
.assertEqual(lines
[0], 'Anne Person <aperson@example.com>\n')
313 self
.assertEqual(lines
[1], 'Bart Person <bperson@example.com>\n')
315 def test_sync_no_display_name(self
):
316 subscribe(self
._mlist
, 'Bart')
317 subscribe(self
._mlist
, 'Cate', role
=MemberRole
.nonmember
)
318 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
319 print('<aperson@example.com>', file=infp
)
320 result
= self
._command
.invoke(members
, (
321 '--sync', infp
.name
, 'ant.example.com'))
324 'Usage: members [OPTIONS] LISTSPEC\n'
325 'Try \'members --help\' for help.\n\n'
326 'Error: The --sync option is removed. '
327 'Use `mailman syncmembers` instead.\n')
329 with
NamedTemporaryFile('w', encoding
='utf-8') as outfp
:
330 self
._command
.invoke(members
, (
331 '-o', outfp
.name
, 'ant.example.com'))
332 with
open(outfp
.name
, 'r', encoding
='utf-8') as infp
:
333 lines
= infp
.readlines()
334 self
.assertEqual(len(lines
), 1)
335 self
.assertEqual(lines
[0], 'Bart Person <bperson@example.com>\n')
337 def test_sync_del_no_display_name(self
):
338 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
339 print('bperson@example.com', file=infp
)
340 result
= self
._command
.invoke(members
, (
341 '--add', infp
.name
, 'ant.example.com'))
343 with
NamedTemporaryFile('w', buffering
=1, encoding
='utf-8') as infp
:
344 print('<aperson@example.com>', file=infp
)
345 result
= self
._command
.invoke(members
, (
346 '--sync', infp
.name
, 'ant.example.com'))
349 'Usage: members [OPTIONS] LISTSPEC\n'
350 'Try \'members --help\' for help.\n\n'
351 'Error: The --sync option is removed. '
352 'Use `mailman syncmembers` instead.\n')
354 with
NamedTemporaryFile('w', encoding
='utf-8') as outfp
:
355 self
._command
.invoke(members
, (
356 '-o', outfp
.name
, 'ant.example.com'))
357 with
open(outfp
.name
, 'r', encoding
='utf-8') as infp
:
358 lines
= infp
.readlines()
359 self
.assertEqual(len(lines
), 1)
360 self
.assertEqual(lines
[0], 'ant.example.com has no members\n')
362 def test_email_only(self
):
363 subscribe(self
._mlist
, 'Anne')
364 subscribe(self
._mlist
, 'Bart')
365 result
= self
._command
.invoke(members
, (
366 '--email-only', 'ant.example.com'))
368 result
.output
, 'aperson@example.com\nbperson@example.com\n')
370 def test_count_only(self
):
371 subscribe(self
._mlist
, 'Anne')
372 subscribe(self
._mlist
, 'Bart')
373 result
= self
._command
.invoke(members
, (
374 '--count-only', 'ant.example.com'))
376 result
.output
, '2\n')
378 def test_incompatible_email_only_count_only(self
):
379 result
= self
._command
.invoke(members
, (
380 '--email-only', '--count-only', 'ant.example.com'))
383 'Usage: members [OPTIONS] LISTSPEC\n'
384 'Try \'members --help\' for help.\n\n'
385 'Error: The --email_only and --count_only options are '
386 'mutually exclusive.\n')
388 def test_incompatible_role_any_regular(self
):
389 result
= self
._command
.invoke(members
, (
390 '--role', 'any', '--regular', 'ant.example.com'))
393 'Usage: members [OPTIONS] LISTSPEC\n'
394 'Try \'members --help\' for help.\n\n'
395 'Error: The --regular, --digest and --nomail options are '
396 'incompatible with role=any.\n')
398 def test_incompatible_role_any_digest(self
):
399 result
= self
._command
.invoke(members
, (
400 '--role', 'any', '--digest', 'any', 'ant.example.com'))
403 'Usage: members [OPTIONS] LISTSPEC\n'
404 'Try \'members --help\' for help.\n\n'
405 'Error: The --regular, --digest and --nomail options are '
406 'incompatible with role=any.\n')
408 def test_incompatible_role_any_nomail(self
):
409 result
= self
._command
.invoke(members
, (
410 '--role', 'any', '--nomail', 'any', 'ant.example.com'))
413 'Usage: members [OPTIONS] LISTSPEC\n'
414 'Try \'members --help\' for help.\n\n'
415 'Error: The --regular, --digest and --nomail options are '
416 'incompatible with role=any.\n')
418 def test_non_ascii_display_name(self
):
419 subscribe(self
._mlist
, 'Bögüs', role
=MemberRole
.member
)
420 with
NamedTemporaryFile('w', encoding
='utf-8') as outfp
:
421 self
._command
.invoke(members
, (
422 '-o', outfp
.name
, 'ant.example.com'))
423 with
open(outfp
.name
, 'r', encoding
='utf-8') as infp
:
424 lines
= infp
.readlines()
425 self
.assertEqual(len(lines
), 1)
426 self
.assertEqual(lines
[0], 'Bögüs Person <bperson@example.com>\n')