blob: 6aa0cb5941eb87d924802ea7df2ab33dd4ead35b [file] [log] [blame]
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001import io
2import datetime
3import textwrap
4import unittest
5import contextlib
6from test import support
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00007from nntplib import NNTP, GroupInfo, _have_ssl
Antoine Pitrou69ab9512010-09-29 15:03:40 +00008import nntplib
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00009if _have_ssl:
10 import ssl
Antoine Pitrou69ab9512010-09-29 15:03:40 +000011
12TIMEOUT = 30
13
14# TODO:
15# - test the `file` arg to more commands
16# - test error conditions
Antoine Pitroua5785b12010-09-29 16:19:50 +000017# - test auth and `usenetrc`
Antoine Pitrou69ab9512010-09-29 15:03:40 +000018
19
20class NetworkedNNTPTestsMixin:
21
22 def test_welcome(self):
23 welcome = self.server.getwelcome()
24 self.assertEqual(str, type(welcome))
25
26 def test_help(self):
Antoine Pitrou08eeada2010-11-04 21:36:15 +000027 resp, lines = self.server.help()
Antoine Pitrou69ab9512010-09-29 15:03:40 +000028 self.assertTrue(resp.startswith("100 "), resp)
Antoine Pitrou08eeada2010-11-04 21:36:15 +000029 for line in lines:
Antoine Pitrou69ab9512010-09-29 15:03:40 +000030 self.assertEqual(str, type(line))
31
32 def test_list(self):
Antoine Pitrou08eeada2010-11-04 21:36:15 +000033 resp, groups = self.server.list()
34 if len(groups) > 0:
35 self.assertEqual(GroupInfo, type(groups[0]))
36 self.assertEqual(str, type(groups[0].group))
37
38 def test_list_active(self):
39 resp, groups = self.server.list(self.GROUP_PAT)
40 if len(groups) > 0:
41 self.assertEqual(GroupInfo, type(groups[0]))
42 self.assertEqual(str, type(groups[0].group))
Antoine Pitrou69ab9512010-09-29 15:03:40 +000043
44 def test_unknown_command(self):
45 with self.assertRaises(nntplib.NNTPPermanentError) as cm:
46 self.server._shortcmd("XYZZY")
47 resp = cm.exception.response
48 self.assertTrue(resp.startswith("500 "), resp)
49
50 def test_newgroups(self):
51 # gmane gets a constant influx of new groups. In order not to stress
52 # the server too much, we choose a recent date in the past.
53 dt = datetime.date.today() - datetime.timedelta(days=7)
54 resp, groups = self.server.newgroups(dt)
55 if len(groups) > 0:
56 self.assertIsInstance(groups[0], GroupInfo)
57 self.assertIsInstance(groups[0].group, str)
58
59 def test_description(self):
60 def _check_desc(desc):
61 # Sanity checks
62 self.assertIsInstance(desc, str)
63 self.assertNotIn(self.GROUP_NAME, desc)
64 desc = self.server.description(self.GROUP_NAME)
65 _check_desc(desc)
66 # Another sanity check
67 self.assertIn("Python", desc)
68 # With a pattern
69 desc = self.server.description(self.GROUP_PAT)
70 _check_desc(desc)
71 # Shouldn't exist
72 desc = self.server.description("zk.brrtt.baz")
73 self.assertEqual(desc, '')
74
75 def test_descriptions(self):
76 resp, descs = self.server.descriptions(self.GROUP_PAT)
77 # 215 for LIST NEWSGROUPS, 282 for XGTITLE
78 self.assertTrue(
79 resp.startswith("215 ") or resp.startswith("282 "), resp)
80 self.assertIsInstance(descs, dict)
81 desc = descs[self.GROUP_NAME]
82 self.assertEqual(desc, self.server.description(self.GROUP_NAME))
83
84 def test_group(self):
85 result = self.server.group(self.GROUP_NAME)
86 self.assertEqual(5, len(result))
87 resp, count, first, last, group = result
88 self.assertEqual(group, self.GROUP_NAME)
89 self.assertIsInstance(count, int)
90 self.assertIsInstance(first, int)
91 self.assertIsInstance(last, int)
92 self.assertLessEqual(first, last)
93 self.assertTrue(resp.startswith("211 "), resp)
94
95 def test_date(self):
96 resp, date = self.server.date()
97 self.assertIsInstance(date, datetime.datetime)
98 # Sanity check
99 self.assertGreaterEqual(date.year, 1995)
100 self.assertLessEqual(date.year, 2030)
101
102 def _check_art_dict(self, art_dict):
103 # Some sanity checks for a field dictionary returned by OVER / XOVER
104 self.assertIsInstance(art_dict, dict)
105 # NNTP has 7 mandatory fields
106 self.assertGreaterEqual(art_dict.keys(),
107 {"subject", "from", "date", "message-id",
108 "references", ":bytes", ":lines"}
109 )
110 for v in art_dict.values():
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000111 self.assertIsInstance(v, (str, type(None)))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000112
113 def test_xover(self):
114 resp, count, first, last, name = self.server.group(self.GROUP_NAME)
115 resp, lines = self.server.xover(last, last)
116 art_num, art_dict = lines[0]
117 self.assertEqual(art_num, last)
118 self._check_art_dict(art_dict)
119
120 def test_over(self):
121 resp, count, first, last, name = self.server.group(self.GROUP_NAME)
122 start = last - 10
123 # The "start-" article range form
124 resp, lines = self.server.over((start, None))
125 art_num, art_dict = lines[0]
126 self._check_art_dict(art_dict)
127 # The "start-end" article range form
128 resp, lines = self.server.over((start, last))
129 art_num, art_dict = lines[-1]
130 self.assertEqual(art_num, last)
131 self._check_art_dict(art_dict)
132 # XXX The "message_id" form is unsupported by gmane
133 # 503 Overview by message-ID unsupported
134
135 def test_xhdr(self):
136 resp, count, first, last, name = self.server.group(self.GROUP_NAME)
137 resp, lines = self.server.xhdr('subject', last)
138 for line in lines:
139 self.assertEqual(str, type(line[1]))
140
141 def check_article_resp(self, resp, article, art_num=None):
142 self.assertIsInstance(article, nntplib.ArticleInfo)
143 if art_num is not None:
144 self.assertEqual(article.number, art_num)
145 for line in article.lines:
146 self.assertIsInstance(line, bytes)
147 # XXX this could exceptionally happen...
148 self.assertNotIn(article.lines[-1], (b".", b".\n", b".\r\n"))
149
150 def test_article_head_body(self):
151 resp, count, first, last, name = self.server.group(self.GROUP_NAME)
152 resp, head = self.server.head(last)
153 self.assertTrue(resp.startswith("221 "), resp)
154 self.check_article_resp(resp, head, last)
155 resp, body = self.server.body(last)
156 self.assertTrue(resp.startswith("222 "), resp)
157 self.check_article_resp(resp, body, last)
158 resp, article = self.server.article(last)
159 self.assertTrue(resp.startswith("220 "), resp)
160 self.check_article_resp(resp, article, last)
161 self.assertEqual(article.lines, head.lines + [b''] + body.lines)
162
163 def test_quit(self):
164 self.server.quit()
165 self.server = None
166
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000167 def test_login(self):
168 baduser = "notarealuser"
169 badpw = "notarealpassword"
170 # Check that bogus credentials cause failure
171 self.assertRaises(nntplib.NNTPError, self.server.login,
172 user=baduser, password=badpw, usenetrc=False)
173 # FIXME: We should check that correct credentials succeed, but that
174 # would require valid details for some server somewhere to be in the
175 # test suite, I think. Gmane is anonymous, at least as used for the
176 # other tests.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000177
178 def test_capabilities(self):
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000179 # The server under test implements NNTP version 2 and has a
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000180 # couple of well-known capabilities. Just sanity check that we
181 # got them.
182 def _check_caps(caps):
183 caps_list = caps['LIST']
184 self.assertIsInstance(caps_list, (list, tuple))
185 self.assertIn('OVERVIEW.FMT', caps_list)
186 self.assertGreaterEqual(self.server.nntp_version, 2)
187 _check_caps(self.server.getcapabilities())
188 # This re-emits the command
189 resp, caps = self.server.capabilities()
190 _check_caps(caps)
191
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000192 if _have_ssl:
193 def test_starttls(self):
194 file = self.server.file
195 sock = self.server.sock
196 try:
197 self.server.starttls()
198 except nntplib.NNTPPermanentError:
199 self.skipTest("STARTTLS not supported by server.")
200 else:
201 # Check that the socket and internal pseudo-file really were
202 # changed.
203 self.assertNotEqual(file, self.server.file)
204 self.assertNotEqual(sock, self.server.sock)
205 # Check that the new socket really is an SSL one
206 self.assertIsInstance(self.server.sock, ssl.SSLSocket)
207 # Check that trying starttls when it's already active fails.
208 self.assertRaises(ValueError, self.server.starttls)
209
210
211class NetworkedNNTPTests(NetworkedNNTPTestsMixin, unittest.TestCase):
212 # This server supports STARTTLS (gmane doesn't)
213 NNTP_HOST = 'news.trigofacile.com'
214 GROUP_NAME = 'fr.comp.lang.python'
215 GROUP_PAT = 'fr.comp.lang.*'
216
217 def setUp(self):
218 support.requires("network")
219 with support.transient_internet(self.NNTP_HOST):
220 self.server = NNTP(self.NNTP_HOST, timeout=TIMEOUT, usenetrc=False)
221
222 def tearDown(self):
223 if self.server is not None:
224 self.server.quit()
225
226
227if _have_ssl:
228 class NetworkedNNTP_SSLTests(NetworkedNNTPTestsMixin, unittest.TestCase):
229 NNTP_HOST = 'snews.gmane.org'
230 GROUP_NAME = 'gmane.comp.python.devel'
231 GROUP_PAT = 'gmane.comp.python.d*'
232
233 def setUp(self):
234 support.requires("network")
235 with support.transient_internet(self.NNTP_HOST):
236 self.server = nntplib.NNTP_SSL(self.NNTP_HOST, timeout=TIMEOUT,
237 usenetrc=False)
238
239 def tearDown(self):
240 if self.server is not None:
241 self.server.quit()
242
243 # Disabled with gmane as it produces too much data
244 test_list = None
245
246 # Disabled as the connection will already be encrypted.
247 test_starttls = None
248
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000249
250#
251# Non-networked tests using a local server (or something mocking it).
252#
253
254class _NNTPServerIO(io.RawIOBase):
255 """A raw IO object allowing NNTP commands to be received and processed
256 by a handler. The handler can push responses which can then be read
257 from the IO object."""
258
259 def __init__(self, handler):
260 io.RawIOBase.__init__(self)
261 # The channel from the client
262 self.c2s = io.BytesIO()
263 # The channel to the client
264 self.s2c = io.BytesIO()
265 self.handler = handler
266 self.handler.start(self.c2s.readline, self.push_data)
267
268 def readable(self):
269 return True
270
271 def writable(self):
272 return True
273
274 def push_data(self, data):
275 """Push (buffer) some data to send to the client."""
276 pos = self.s2c.tell()
277 self.s2c.seek(0, 2)
278 self.s2c.write(data)
279 self.s2c.seek(pos)
280
281 def write(self, b):
282 """The client sends us some data"""
283 pos = self.c2s.tell()
284 self.c2s.write(b)
285 self.c2s.seek(pos)
286 self.handler.process_pending()
287 return len(b)
288
289 def readinto(self, buf):
290 """The client wants to read a response"""
291 self.handler.process_pending()
292 b = self.s2c.read(len(buf))
293 n = len(b)
294 buf[:n] = b
295 return n
296
297
298class MockedNNTPTestsMixin:
299 # Override in derived classes
300 handler_class = None
301
302 def setUp(self):
303 super().setUp()
304 self.make_server()
305
306 def tearDown(self):
307 super().tearDown()
308 del self.server
309
310 def make_server(self, *args, **kwargs):
311 self.handler = self.handler_class()
312 self.sio = _NNTPServerIO(self.handler)
313 # Using BufferedRWPair instead of BufferedRandom ensures the file
314 # isn't seekable.
315 file = io.BufferedRWPair(self.sio, self.sio)
Antoine Pitroua5785b12010-09-29 16:19:50 +0000316 self.server = nntplib._NNTPBase(file, 'test.server', *args, **kwargs)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000317 return self.server
318
319
320class NNTPv1Handler:
321 """A handler for RFC 977"""
322
323 welcome = "200 NNTP mock server"
324
325 def start(self, readline, push_data):
326 self.in_body = False
327 self.allow_posting = True
328 self._readline = readline
329 self._push_data = push_data
330 # Our welcome
331 self.handle_welcome()
332
333 def _decode(self, data):
334 return str(data, "utf-8", "surrogateescape")
335
336 def process_pending(self):
337 if self.in_body:
338 while True:
339 line = self._readline()
340 if not line:
341 return
342 self.body.append(line)
343 if line == b".\r\n":
344 break
345 try:
346 meth, tokens = self.body_callback
347 meth(*tokens, body=self.body)
348 finally:
349 self.body_callback = None
350 self.body = None
351 self.in_body = False
352 while True:
353 line = self._decode(self._readline())
354 if not line:
355 return
356 if not line.endswith("\r\n"):
357 raise ValueError("line doesn't end with \\r\\n: {!r}".format(line))
358 line = line[:-2]
359 cmd, *tokens = line.split()
360 #meth = getattr(self.handler, "handle_" + cmd.upper(), None)
361 meth = getattr(self, "handle_" + cmd.upper(), None)
362 if meth is None:
363 self.handle_unknown()
364 else:
365 try:
366 meth(*tokens)
367 except Exception as e:
368 raise ValueError("command failed: {!r}".format(line)) from e
369 else:
370 if self.in_body:
371 self.body_callback = meth, tokens
372 self.body = []
373
374 def expect_body(self):
375 """Flag that the client is expected to post a request body"""
376 self.in_body = True
377
378 def push_data(self, data):
379 """Push some binary data"""
380 self._push_data(data)
381
382 def push_lit(self, lit):
383 """Push a string literal"""
384 lit = textwrap.dedent(lit)
385 lit = "\r\n".join(lit.splitlines()) + "\r\n"
386 lit = lit.encode('utf-8')
387 self.push_data(lit)
388
389 def handle_unknown(self):
390 self.push_lit("500 What?")
391
392 def handle_welcome(self):
393 self.push_lit(self.welcome)
394
395 def handle_QUIT(self):
396 self.push_lit("205 Bye!")
397
398 def handle_DATE(self):
399 self.push_lit("111 20100914001155")
400
401 def handle_GROUP(self, group):
402 if group == "fr.comp.lang.python":
403 self.push_lit("211 486 761 1265 fr.comp.lang.python")
404 else:
405 self.push_lit("411 No such group {}".format(group))
406
407 def handle_HELP(self):
408 self.push_lit("""\
409 100 Legal commands
410 authinfo user Name|pass Password|generic <prog> <args>
411 date
412 help
413 Report problems to <root@example.org>
414 .""")
415
416 def handle_STAT(self, message_spec=None):
417 if message_spec is None:
418 self.push_lit("412 No newsgroup selected")
419 elif message_spec == "3000234":
420 self.push_lit("223 3000234 <45223423@example.com>")
421 elif message_spec == "<45223423@example.com>":
422 self.push_lit("223 0 <45223423@example.com>")
423 else:
424 self.push_lit("430 No Such Article Found")
425
426 def handle_NEXT(self):
427 self.push_lit("223 3000237 <668929@example.org> retrieved")
428
429 def handle_LAST(self):
430 self.push_lit("223 3000234 <45223423@example.com> retrieved")
431
432 def handle_LIST(self, action=None, param=None):
433 if action is None:
434 self.push_lit("""\
435 215 Newsgroups in form "group high low flags".
436 comp.lang.python 0000052340 0000002828 y
437 comp.lang.python.announce 0000001153 0000000993 m
438 free.it.comp.lang.python 0000000002 0000000002 y
439 fr.comp.lang.python 0000001254 0000000760 y
440 free.it.comp.lang.python.learner 0000000000 0000000001 y
441 tw.bbs.comp.lang.python 0000000304 0000000304 y
442 .""")
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000443 elif action == "ACTIVE":
444 if param == "*distutils*":
445 self.push_lit("""\
446 215 Newsgroups in form "group high low flags"
447 gmane.comp.python.distutils.devel 0000014104 0000000001 m
448 gmane.comp.python.distutils.cvs 0000000000 0000000001 m
449 .""")
450 else:
451 self.push_lit("""\
452 215 Newsgroups in form "group high low flags"
453 .""")
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000454 elif action == "OVERVIEW.FMT":
455 self.push_lit("""\
456 215 Order of fields in overview database.
457 Subject:
458 From:
459 Date:
460 Message-ID:
461 References:
462 Bytes:
463 Lines:
464 Xref:full
465 .""")
466 elif action == "NEWSGROUPS":
467 assert param is not None
468 if param == "comp.lang.python":
469 self.push_lit("""\
470 215 Descriptions in form "group description".
471 comp.lang.python\tThe Python computer language.
472 .""")
473 elif param == "comp.lang.python*":
474 self.push_lit("""\
475 215 Descriptions in form "group description".
476 comp.lang.python.announce\tAnnouncements about the Python language. (Moderated)
477 comp.lang.python\tThe Python computer language.
478 .""")
479 else:
480 self.push_lit("""\
481 215 Descriptions in form "group description".
482 .""")
483 else:
484 self.push_lit('501 Unknown LIST keyword')
485
486 def handle_NEWNEWS(self, group, date_str, time_str):
487 # We hard code different return messages depending on passed
488 # argument and date syntax.
489 if (group == "comp.lang.python" and date_str == "20100913"
490 and time_str == "082004"):
491 # Date was passed in RFC 3977 format (NNTP "v2")
492 self.push_lit("""\
493 230 list of newsarticles (NNTP v2) created after Mon Sep 13 08:20:04 2010 follows
494 <a4929a40-6328-491a-aaaf-cb79ed7309a2@q2g2000vbk.googlegroups.com>
495 <f30c0419-f549-4218-848f-d7d0131da931@y3g2000vbm.googlegroups.com>
496 .""")
497 elif (group == "comp.lang.python" and date_str == "100913"
498 and time_str == "082004"):
499 # Date was passed in RFC 977 format (NNTP "v1")
500 self.push_lit("""\
501 230 list of newsarticles (NNTP v1) created after Mon Sep 13 08:20:04 2010 follows
502 <a4929a40-6328-491a-aaaf-cb79ed7309a2@q2g2000vbk.googlegroups.com>
503 <f30c0419-f549-4218-848f-d7d0131da931@y3g2000vbm.googlegroups.com>
504 .""")
505 else:
506 self.push_lit("""\
507 230 An empty list of newsarticles follows
508 .""")
509 # (Note for experiments: many servers disable NEWNEWS.
510 # As of this writing, sicinfo3.epfl.ch doesn't.)
511
512 def handle_XOVER(self, message_spec):
513 if message_spec == "57-59":
514 self.push_lit(
515 "224 Overview information for 57-58 follows\n"
516 "57\tRe: ANN: New Plone book with strong Python (and Zope) themes throughout"
517 "\tDoug Hellmann <doug.hellmann-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>"
518 "\tSat, 19 Jun 2010 18:04:08 -0400"
519 "\t<4FD05F05-F98B-44DC-8111-C6009C925F0C@gmail.com>"
520 "\t<hvalf7$ort$1@dough.gmane.org>\t7103\t16"
521 "\tXref: news.gmane.org gmane.comp.python.authors:57"
522 "\n"
523 "58\tLooking for a few good bloggers"
524 "\tDoug Hellmann <doug.hellmann-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>"
525 "\tThu, 22 Jul 2010 09:14:14 -0400"
526 "\t<A29863FA-F388-40C3-AA25-0FD06B09B5BF@gmail.com>"
527 "\t\t6683\t16"
Antoine Pitrou4103bc02010-11-03 18:18:43 +0000528 "\t"
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000529 "\n"
530 # An UTF-8 overview line from fr.comp.lang.python
531 "59\tRe: Message d'erreur incompréhensible (par moi)"
532 "\tEric Brunel <eric.brunel@pragmadev.nospam.com>"
533 "\tWed, 15 Sep 2010 18:09:15 +0200"
534 "\t<eric.brunel-2B8B56.18091515092010@news.wanadoo.fr>"
535 "\t<4c90ec87$0$32425$ba4acef3@reader.news.orange.fr>\t1641\t27"
536 "\tXref: saria.nerim.net fr.comp.lang.python:1265"
537 "\n"
538 ".\n")
539 else:
540 self.push_lit("""\
541 224 No articles
542 .""")
543
544 def handle_POST(self, *, body=None):
545 if body is None:
546 if self.allow_posting:
547 self.push_lit("340 Input article; end with <CR-LF>.<CR-LF>")
548 self.expect_body()
549 else:
550 self.push_lit("440 Posting not permitted")
551 else:
552 assert self.allow_posting
553 self.push_lit("240 Article received OK")
554 self.posted_body = body
555
556 def handle_IHAVE(self, message_id, *, body=None):
557 if body is None:
558 if (self.allow_posting and
559 message_id == "<i.am.an.article.you.will.want@example.com>"):
560 self.push_lit("335 Send it; end with <CR-LF>.<CR-LF>")
561 self.expect_body()
562 else:
563 self.push_lit("435 Article not wanted")
564 else:
565 assert self.allow_posting
566 self.push_lit("235 Article transferred OK")
567 self.posted_body = body
568
569 sample_head = """\
570 From: "Demo User" <nobody@example.net>
571 Subject: I am just a test article
572 Content-Type: text/plain; charset=UTF-8; format=flowed
573 Message-ID: <i.am.an.article.you.will.want@example.com>"""
574
575 sample_body = """\
576 This is just a test article.
577 ..Here is a dot-starting line.
578
579 -- Signed by Andr\xe9."""
580
581 sample_article = sample_head + "\n\n" + sample_body
582
583 def handle_ARTICLE(self, message_spec=None):
584 if message_spec is None:
585 self.push_lit("220 3000237 <45223423@example.com>")
586 elif message_spec == "<45223423@example.com>":
587 self.push_lit("220 0 <45223423@example.com>")
588 elif message_spec == "3000234":
589 self.push_lit("220 3000234 <45223423@example.com>")
590 else:
591 self.push_lit("430 No Such Article Found")
592 return
593 self.push_lit(self.sample_article)
594 self.push_lit(".")
595
596 def handle_HEAD(self, message_spec=None):
597 if message_spec is None:
598 self.push_lit("221 3000237 <45223423@example.com>")
599 elif message_spec == "<45223423@example.com>":
600 self.push_lit("221 0 <45223423@example.com>")
601 elif message_spec == "3000234":
602 self.push_lit("221 3000234 <45223423@example.com>")
603 else:
604 self.push_lit("430 No Such Article Found")
605 return
606 self.push_lit(self.sample_head)
607 self.push_lit(".")
608
609 def handle_BODY(self, message_spec=None):
610 if message_spec is None:
611 self.push_lit("222 3000237 <45223423@example.com>")
612 elif message_spec == "<45223423@example.com>":
613 self.push_lit("222 0 <45223423@example.com>")
614 elif message_spec == "3000234":
615 self.push_lit("222 3000234 <45223423@example.com>")
616 else:
617 self.push_lit("430 No Such Article Found")
618 return
619 self.push_lit(self.sample_body)
620 self.push_lit(".")
621
622
623class NNTPv2Handler(NNTPv1Handler):
624 """A handler for RFC 3977 (NNTP "v2")"""
625
626 def handle_CAPABILITIES(self):
627 self.push_lit("""\
628 101 Capability list:
Antoine Pitrouf80b3f72010-11-02 22:31:52 +0000629 VERSION 2 3
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000630 IMPLEMENTATION INN 2.5.1
631 AUTHINFO USER
632 HDR
633 LIST ACTIVE ACTIVE.TIMES DISTRIB.PATS HEADERS NEWSGROUPS OVERVIEW.FMT
634 OVER
635 POST
636 READER
637 .""")
638
639 def handle_OVER(self, message_spec=None):
640 return self.handle_XOVER(message_spec)
641
642
643class NNTPv1v2TestsMixin:
644
645 def setUp(self):
646 super().setUp()
647
648 def test_welcome(self):
649 self.assertEqual(self.server.welcome, self.handler.welcome)
650
651 def test_date(self):
652 resp, date = self.server.date()
653 self.assertEqual(resp, "111 20100914001155")
654 self.assertEqual(date, datetime.datetime(2010, 9, 14, 0, 11, 55))
655
656 def test_quit(self):
657 self.assertFalse(self.sio.closed)
658 resp = self.server.quit()
659 self.assertEqual(resp, "205 Bye!")
660 self.assertTrue(self.sio.closed)
661
662 def test_help(self):
663 resp, help = self.server.help()
664 self.assertEqual(resp, "100 Legal commands")
665 self.assertEqual(help, [
666 ' authinfo user Name|pass Password|generic <prog> <args>',
667 ' date',
668 ' help',
669 'Report problems to <root@example.org>',
670 ])
671
672 def test_list(self):
673 resp, groups = self.server.list()
674 self.assertEqual(len(groups), 6)
675 g = groups[1]
676 self.assertEqual(g,
677 GroupInfo("comp.lang.python.announce", "0000001153",
678 "0000000993", "m"))
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000679 resp, groups = self.server.list("*distutils*")
680 self.assertEqual(len(groups), 2)
681 g = groups[0]
682 self.assertEqual(g,
683 GroupInfo("gmane.comp.python.distutils.devel", "0000014104",
684 "0000000001", "m"))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000685
686 def test_stat(self):
687 resp, art_num, message_id = self.server.stat(3000234)
688 self.assertEqual(resp, "223 3000234 <45223423@example.com>")
689 self.assertEqual(art_num, 3000234)
690 self.assertEqual(message_id, "<45223423@example.com>")
691 resp, art_num, message_id = self.server.stat("<45223423@example.com>")
692 self.assertEqual(resp, "223 0 <45223423@example.com>")
693 self.assertEqual(art_num, 0)
694 self.assertEqual(message_id, "<45223423@example.com>")
695 with self.assertRaises(nntplib.NNTPTemporaryError) as cm:
696 self.server.stat("<non.existent.id>")
697 self.assertEqual(cm.exception.response, "430 No Such Article Found")
698 with self.assertRaises(nntplib.NNTPTemporaryError) as cm:
699 self.server.stat()
700 self.assertEqual(cm.exception.response, "412 No newsgroup selected")
701
702 def test_next(self):
703 resp, art_num, message_id = self.server.next()
704 self.assertEqual(resp, "223 3000237 <668929@example.org> retrieved")
705 self.assertEqual(art_num, 3000237)
706 self.assertEqual(message_id, "<668929@example.org>")
707
708 def test_last(self):
709 resp, art_num, message_id = self.server.last()
710 self.assertEqual(resp, "223 3000234 <45223423@example.com> retrieved")
711 self.assertEqual(art_num, 3000234)
712 self.assertEqual(message_id, "<45223423@example.com>")
713
714 def test_description(self):
715 desc = self.server.description("comp.lang.python")
716 self.assertEqual(desc, "The Python computer language.")
717 desc = self.server.description("comp.lang.pythonx")
718 self.assertEqual(desc, "")
719
720 def test_descriptions(self):
721 resp, groups = self.server.descriptions("comp.lang.python")
722 self.assertEqual(resp, '215 Descriptions in form "group description".')
723 self.assertEqual(groups, {
724 "comp.lang.python": "The Python computer language.",
725 })
726 resp, groups = self.server.descriptions("comp.lang.python*")
727 self.assertEqual(groups, {
728 "comp.lang.python": "The Python computer language.",
729 "comp.lang.python.announce": "Announcements about the Python language. (Moderated)",
730 })
731 resp, groups = self.server.descriptions("comp.lang.pythonx")
732 self.assertEqual(groups, {})
733
734 def test_group(self):
735 resp, count, first, last, group = self.server.group("fr.comp.lang.python")
736 self.assertTrue(resp.startswith("211 "), resp)
737 self.assertEqual(first, 761)
738 self.assertEqual(last, 1265)
739 self.assertEqual(count, 486)
740 self.assertEqual(group, "fr.comp.lang.python")
741 with self.assertRaises(nntplib.NNTPTemporaryError) as cm:
742 self.server.group("comp.lang.python.devel")
743 exc = cm.exception
744 self.assertTrue(exc.response.startswith("411 No such group"),
745 exc.response)
746
747 def test_newnews(self):
748 # NEWNEWS comp.lang.python [20]100913 082004
749 dt = datetime.datetime(2010, 9, 13, 8, 20, 4)
750 resp, ids = self.server.newnews("comp.lang.python", dt)
751 expected = (
752 "230 list of newsarticles (NNTP v{0}) "
753 "created after Mon Sep 13 08:20:04 2010 follows"
754 ).format(self.nntp_version)
755 self.assertEqual(resp, expected)
756 self.assertEqual(ids, [
757 "<a4929a40-6328-491a-aaaf-cb79ed7309a2@q2g2000vbk.googlegroups.com>",
758 "<f30c0419-f549-4218-848f-d7d0131da931@y3g2000vbm.googlegroups.com>",
759 ])
760 # NEWNEWS fr.comp.lang.python [20]100913 082004
761 dt = datetime.datetime(2010, 9, 13, 8, 20, 4)
762 resp, ids = self.server.newnews("fr.comp.lang.python", dt)
763 self.assertEqual(resp, "230 An empty list of newsarticles follows")
764 self.assertEqual(ids, [])
765
766 def _check_article_body(self, lines):
767 self.assertEqual(len(lines), 4)
768 self.assertEqual(lines[-1].decode('utf8'), "-- Signed by André.")
769 self.assertEqual(lines[-2], b"")
770 self.assertEqual(lines[-3], b".Here is a dot-starting line.")
771 self.assertEqual(lines[-4], b"This is just a test article.")
772
773 def _check_article_head(self, lines):
774 self.assertEqual(len(lines), 4)
775 self.assertEqual(lines[0], b'From: "Demo User" <nobody@example.net>')
776 self.assertEqual(lines[3], b"Message-ID: <i.am.an.article.you.will.want@example.com>")
777
778 def _check_article_data(self, lines):
779 self.assertEqual(len(lines), 9)
780 self._check_article_head(lines[:4])
781 self._check_article_body(lines[-4:])
782 self.assertEqual(lines[4], b"")
783
784 def test_article(self):
785 # ARTICLE
786 resp, info = self.server.article()
787 self.assertEqual(resp, "220 3000237 <45223423@example.com>")
788 art_num, message_id, lines = info
789 self.assertEqual(art_num, 3000237)
790 self.assertEqual(message_id, "<45223423@example.com>")
791 self._check_article_data(lines)
792 # ARTICLE num
793 resp, info = self.server.article(3000234)
794 self.assertEqual(resp, "220 3000234 <45223423@example.com>")
795 art_num, message_id, lines = info
796 self.assertEqual(art_num, 3000234)
797 self.assertEqual(message_id, "<45223423@example.com>")
798 self._check_article_data(lines)
799 # ARTICLE id
800 resp, info = self.server.article("<45223423@example.com>")
801 self.assertEqual(resp, "220 0 <45223423@example.com>")
802 art_num, message_id, lines = info
803 self.assertEqual(art_num, 0)
804 self.assertEqual(message_id, "<45223423@example.com>")
805 self._check_article_data(lines)
806 # Non-existent id
807 with self.assertRaises(nntplib.NNTPTemporaryError) as cm:
808 self.server.article("<non-existent@example.com>")
809 self.assertEqual(cm.exception.response, "430 No Such Article Found")
810
811 def test_article_file(self):
812 # With a "file" argument
813 f = io.BytesIO()
814 resp, info = self.server.article(file=f)
815 self.assertEqual(resp, "220 3000237 <45223423@example.com>")
816 art_num, message_id, lines = info
817 self.assertEqual(art_num, 3000237)
818 self.assertEqual(message_id, "<45223423@example.com>")
819 self.assertEqual(lines, [])
820 data = f.getvalue()
821 self.assertTrue(data.startswith(
822 b'From: "Demo User" <nobody@example.net>\r\n'
823 b'Subject: I am just a test article\r\n'
824 ), ascii(data))
825 self.assertTrue(data.endswith(
826 b'This is just a test article.\r\n'
827 b'.Here is a dot-starting line.\r\n'
828 b'\r\n'
829 b'-- Signed by Andr\xc3\xa9.\r\n'
830 ), ascii(data))
831
832 def test_head(self):
833 # HEAD
834 resp, info = self.server.head()
835 self.assertEqual(resp, "221 3000237 <45223423@example.com>")
836 art_num, message_id, lines = info
837 self.assertEqual(art_num, 3000237)
838 self.assertEqual(message_id, "<45223423@example.com>")
839 self._check_article_head(lines)
840 # HEAD num
841 resp, info = self.server.head(3000234)
842 self.assertEqual(resp, "221 3000234 <45223423@example.com>")
843 art_num, message_id, lines = info
844 self.assertEqual(art_num, 3000234)
845 self.assertEqual(message_id, "<45223423@example.com>")
846 self._check_article_head(lines)
847 # HEAD id
848 resp, info = self.server.head("<45223423@example.com>")
849 self.assertEqual(resp, "221 0 <45223423@example.com>")
850 art_num, message_id, lines = info
851 self.assertEqual(art_num, 0)
852 self.assertEqual(message_id, "<45223423@example.com>")
853 self._check_article_head(lines)
854 # Non-existent id
855 with self.assertRaises(nntplib.NNTPTemporaryError) as cm:
856 self.server.head("<non-existent@example.com>")
857 self.assertEqual(cm.exception.response, "430 No Such Article Found")
858
859 def test_body(self):
860 # BODY
861 resp, info = self.server.body()
862 self.assertEqual(resp, "222 3000237 <45223423@example.com>")
863 art_num, message_id, lines = info
864 self.assertEqual(art_num, 3000237)
865 self.assertEqual(message_id, "<45223423@example.com>")
866 self._check_article_body(lines)
867 # BODY num
868 resp, info = self.server.body(3000234)
869 self.assertEqual(resp, "222 3000234 <45223423@example.com>")
870 art_num, message_id, lines = info
871 self.assertEqual(art_num, 3000234)
872 self.assertEqual(message_id, "<45223423@example.com>")
873 self._check_article_body(lines)
874 # BODY id
875 resp, info = self.server.body("<45223423@example.com>")
876 self.assertEqual(resp, "222 0 <45223423@example.com>")
877 art_num, message_id, lines = info
878 self.assertEqual(art_num, 0)
879 self.assertEqual(message_id, "<45223423@example.com>")
880 self._check_article_body(lines)
881 # Non-existent id
882 with self.assertRaises(nntplib.NNTPTemporaryError) as cm:
883 self.server.body("<non-existent@example.com>")
884 self.assertEqual(cm.exception.response, "430 No Such Article Found")
885
886 def check_over_xover_resp(self, resp, overviews):
887 self.assertTrue(resp.startswith("224 "), resp)
888 self.assertEqual(len(overviews), 3)
889 art_num, over = overviews[0]
890 self.assertEqual(art_num, 57)
891 self.assertEqual(over, {
892 "from": "Doug Hellmann <doug.hellmann-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>",
893 "subject": "Re: ANN: New Plone book with strong Python (and Zope) themes throughout",
894 "date": "Sat, 19 Jun 2010 18:04:08 -0400",
895 "message-id": "<4FD05F05-F98B-44DC-8111-C6009C925F0C@gmail.com>",
896 "references": "<hvalf7$ort$1@dough.gmane.org>",
897 ":bytes": "7103",
898 ":lines": "16",
899 "xref": "news.gmane.org gmane.comp.python.authors:57"
900 })
Antoine Pitrou4103bc02010-11-03 18:18:43 +0000901 art_num, over = overviews[1]
902 self.assertEqual(over["xref"], None)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000903 art_num, over = overviews[2]
904 self.assertEqual(over["subject"],
905 "Re: Message d'erreur incompréhensible (par moi)")
906
907 def test_xover(self):
908 resp, overviews = self.server.xover(57, 59)
909 self.check_over_xover_resp(resp, overviews)
910
911 def test_over(self):
912 # In NNTP "v1", this will fallback on XOVER
913 resp, overviews = self.server.over((57, 59))
914 self.check_over_xover_resp(resp, overviews)
915
916 sample_post = (
917 b'From: "Demo User" <nobody@example.net>\r\n'
918 b'Subject: I am just a test article\r\n'
919 b'Content-Type: text/plain; charset=UTF-8; format=flowed\r\n'
920 b'Message-ID: <i.am.an.article.you.will.want@example.com>\r\n'
921 b'\r\n'
922 b'This is just a test article.\r\n'
923 b'.Here is a dot-starting line.\r\n'
924 b'\r\n'
925 b'-- Signed by Andr\xc3\xa9.\r\n'
926 )
927
928 def _check_posted_body(self):
929 # Check the raw body as received by the server
930 lines = self.handler.posted_body
931 # One additional line for the "." terminator
932 self.assertEqual(len(lines), 10)
933 self.assertEqual(lines[-1], b'.\r\n')
934 self.assertEqual(lines[-2], b'-- Signed by Andr\xc3\xa9.\r\n')
935 self.assertEqual(lines[-3], b'\r\n')
936 self.assertEqual(lines[-4], b'..Here is a dot-starting line.\r\n')
937 self.assertEqual(lines[0], b'From: "Demo User" <nobody@example.net>\r\n')
938
939 def _check_post_ihave_sub(self, func, *args, file_factory):
940 # First the prepared post with CRLF endings
941 post = self.sample_post
942 func_args = args + (file_factory(post),)
943 self.handler.posted_body = None
944 resp = func(*func_args)
945 self._check_posted_body()
946 # Then the same post with "normal" line endings - they should be
947 # converted by NNTP.post and NNTP.ihave.
948 post = self.sample_post.replace(b"\r\n", b"\n")
949 func_args = args + (file_factory(post),)
950 self.handler.posted_body = None
951 resp = func(*func_args)
952 self._check_posted_body()
953 return resp
954
955 def check_post_ihave(self, func, success_resp, *args):
956 # With a bytes object
957 resp = self._check_post_ihave_sub(func, *args, file_factory=bytes)
958 self.assertEqual(resp, success_resp)
959 # With a bytearray object
960 resp = self._check_post_ihave_sub(func, *args, file_factory=bytearray)
961 self.assertEqual(resp, success_resp)
962 # With a file object
963 resp = self._check_post_ihave_sub(func, *args, file_factory=io.BytesIO)
964 self.assertEqual(resp, success_resp)
965 # With an iterable of terminated lines
966 def iterlines(b):
967 return iter(b.splitlines(True))
968 resp = self._check_post_ihave_sub(func, *args, file_factory=iterlines)
969 self.assertEqual(resp, success_resp)
970 # With an iterable of non-terminated lines
971 def iterlines(b):
972 return iter(b.splitlines(False))
973 resp = self._check_post_ihave_sub(func, *args, file_factory=iterlines)
974 self.assertEqual(resp, success_resp)
975
976 def test_post(self):
977 self.check_post_ihave(self.server.post, "240 Article received OK")
978 self.handler.allow_posting = False
979 with self.assertRaises(nntplib.NNTPTemporaryError) as cm:
980 self.server.post(self.sample_post)
981 self.assertEqual(cm.exception.response,
982 "440 Posting not permitted")
983
984 def test_ihave(self):
985 self.check_post_ihave(self.server.ihave, "235 Article transferred OK",
986 "<i.am.an.article.you.will.want@example.com>")
987 with self.assertRaises(nntplib.NNTPTemporaryError) as cm:
988 self.server.ihave("<another.message.id>", self.sample_post)
989 self.assertEqual(cm.exception.response,
990 "435 Article not wanted")
991
992
993class NNTPv1Tests(NNTPv1v2TestsMixin, MockedNNTPTestsMixin, unittest.TestCase):
994 """Tests an NNTP v1 server (no capabilities)."""
995
996 nntp_version = 1
997 handler_class = NNTPv1Handler
998
999 def test_caps(self):
1000 caps = self.server.getcapabilities()
1001 self.assertEqual(caps, {})
1002 self.assertEqual(self.server.nntp_version, 1)
Antoine Pitroua0781152010-11-05 19:16:37 +00001003 self.assertEqual(self.server.nntp_implementation, None)
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001004
1005
1006class NNTPv2Tests(NNTPv1v2TestsMixin, MockedNNTPTestsMixin, unittest.TestCase):
1007 """Tests an NNTP v2 server (with capabilities)."""
1008
1009 nntp_version = 2
1010 handler_class = NNTPv2Handler
1011
1012 def test_caps(self):
1013 caps = self.server.getcapabilities()
1014 self.assertEqual(caps, {
Antoine Pitrouf80b3f72010-11-02 22:31:52 +00001015 'VERSION': ['2', '3'],
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001016 'IMPLEMENTATION': ['INN', '2.5.1'],
1017 'AUTHINFO': ['USER'],
1018 'HDR': [],
1019 'LIST': ['ACTIVE', 'ACTIVE.TIMES', 'DISTRIB.PATS',
1020 'HEADERS', 'NEWSGROUPS', 'OVERVIEW.FMT'],
1021 'OVER': [],
1022 'POST': [],
1023 'READER': [],
1024 })
Antoine Pitrouf80b3f72010-11-02 22:31:52 +00001025 self.assertEqual(self.server.nntp_version, 3)
Antoine Pitroua0781152010-11-05 19:16:37 +00001026 self.assertEqual(self.server.nntp_implementation, 'INN 2.5.1')
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001027
1028
1029class MiscTests(unittest.TestCase):
1030
1031 def test_decode_header(self):
1032 def gives(a, b):
1033 self.assertEqual(nntplib.decode_header(a), b)
1034 gives("" , "")
1035 gives("a plain header", "a plain header")
1036 gives(" with extra spaces ", " with extra spaces ")
1037 gives("=?ISO-8859-15?Q?D=E9buter_en_Python?=", "Débuter en Python")
1038 gives("=?utf-8?q?Re=3A_=5Bsqlite=5D_probl=C3=A8me_avec_ORDER_BY_sur_des_cha?="
1039 " =?utf-8?q?=C3=AEnes_de_caract=C3=A8res_accentu=C3=A9es?=",
1040 "Re: [sqlite] problème avec ORDER BY sur des chaînes de caractères accentuées")
1041 gives("Re: =?UTF-8?B?cHJvYmzDqG1lIGRlIG1hdHJpY2U=?=",
1042 "Re: problème de matrice")
1043 # A natively utf-8 header (found in the real world!)
1044 gives("Re: Message d'erreur incompréhensible (par moi)",
1045 "Re: Message d'erreur incompréhensible (par moi)")
1046
1047 def test_parse_overview_fmt(self):
1048 # The minimal (default) response
1049 lines = ["Subject:", "From:", "Date:", "Message-ID:",
1050 "References:", ":bytes", ":lines"]
1051 self.assertEqual(nntplib._parse_overview_fmt(lines),
1052 ["subject", "from", "date", "message-id", "references",
1053 ":bytes", ":lines"])
1054 # The minimal response using alternative names
1055 lines = ["Subject:", "From:", "Date:", "Message-ID:",
1056 "References:", "Bytes:", "Lines:"]
1057 self.assertEqual(nntplib._parse_overview_fmt(lines),
1058 ["subject", "from", "date", "message-id", "references",
1059 ":bytes", ":lines"])
1060 # Variations in casing
1061 lines = ["subject:", "FROM:", "DaTe:", "message-ID:",
1062 "References:", "BYTES:", "Lines:"]
1063 self.assertEqual(nntplib._parse_overview_fmt(lines),
1064 ["subject", "from", "date", "message-id", "references",
1065 ":bytes", ":lines"])
1066 # First example from RFC 3977
1067 lines = ["Subject:", "From:", "Date:", "Message-ID:",
1068 "References:", ":bytes", ":lines", "Xref:full",
1069 "Distribution:full"]
1070 self.assertEqual(nntplib._parse_overview_fmt(lines),
1071 ["subject", "from", "date", "message-id", "references",
1072 ":bytes", ":lines", "xref", "distribution"])
1073 # Second example from RFC 3977
1074 lines = ["Subject:", "From:", "Date:", "Message-ID:",
1075 "References:", "Bytes:", "Lines:", "Xref:FULL",
1076 "Distribution:FULL"]
1077 self.assertEqual(nntplib._parse_overview_fmt(lines),
1078 ["subject", "from", "date", "message-id", "references",
1079 ":bytes", ":lines", "xref", "distribution"])
1080 # A classic response from INN
1081 lines = ["Subject:", "From:", "Date:", "Message-ID:",
1082 "References:", "Bytes:", "Lines:", "Xref:full"]
1083 self.assertEqual(nntplib._parse_overview_fmt(lines),
1084 ["subject", "from", "date", "message-id", "references",
1085 ":bytes", ":lines", "xref"])
1086
1087 def test_parse_overview(self):
1088 fmt = nntplib._DEFAULT_OVERVIEW_FMT + ["xref"]
1089 # First example from RFC 3977
1090 lines = [
1091 '3000234\tI am just a test article\t"Demo User" '
1092 '<nobody@example.com>\t6 Oct 1998 04:38:40 -0500\t'
1093 '<45223423@example.com>\t<45454@example.net>\t1234\t'
1094 '17\tXref: news.example.com misc.test:3000363',
1095 ]
1096 overview = nntplib._parse_overview(lines, fmt)
1097 (art_num, fields), = overview
1098 self.assertEqual(art_num, 3000234)
1099 self.assertEqual(fields, {
1100 'subject': 'I am just a test article',
1101 'from': '"Demo User" <nobody@example.com>',
1102 'date': '6 Oct 1998 04:38:40 -0500',
1103 'message-id': '<45223423@example.com>',
1104 'references': '<45454@example.net>',
1105 ':bytes': '1234',
1106 ':lines': '17',
1107 'xref': 'news.example.com misc.test:3000363',
1108 })
Antoine Pitrou4103bc02010-11-03 18:18:43 +00001109 # Second example; here the "Xref" field is totally absent (including
1110 # the header name) and comes out as None
1111 lines = [
1112 '3000234\tI am just a test article\t"Demo User" '
1113 '<nobody@example.com>\t6 Oct 1998 04:38:40 -0500\t'
1114 '<45223423@example.com>\t<45454@example.net>\t1234\t'
1115 '17\t\t',
1116 ]
1117 overview = nntplib._parse_overview(lines, fmt)
1118 (art_num, fields), = overview
1119 self.assertEqual(fields['xref'], None)
1120 # Third example; the "Xref" is an empty string, while "references"
1121 # is a single space.
1122 lines = [
1123 '3000234\tI am just a test article\t"Demo User" '
1124 '<nobody@example.com>\t6 Oct 1998 04:38:40 -0500\t'
1125 '<45223423@example.com>\t \t1234\t'
1126 '17\tXref: \t',
1127 ]
1128 overview = nntplib._parse_overview(lines, fmt)
1129 (art_num, fields), = overview
1130 self.assertEqual(fields['references'], ' ')
1131 self.assertEqual(fields['xref'], '')
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001132
1133 def test_parse_datetime(self):
1134 def gives(a, b, *c):
1135 self.assertEqual(nntplib._parse_datetime(a, b),
1136 datetime.datetime(*c))
1137 # Output of DATE command
1138 gives("19990623135624", None, 1999, 6, 23, 13, 56, 24)
1139 # Variations
1140 gives("19990623", "135624", 1999, 6, 23, 13, 56, 24)
1141 gives("990623", "135624", 1999, 6, 23, 13, 56, 24)
1142 gives("090623", "135624", 2009, 6, 23, 13, 56, 24)
1143
1144 def test_unparse_datetime(self):
1145 # Test non-legacy mode
1146 # 1) with a datetime
1147 def gives(y, M, d, h, m, s, date_str, time_str):
1148 dt = datetime.datetime(y, M, d, h, m, s)
1149 self.assertEqual(nntplib._unparse_datetime(dt),
1150 (date_str, time_str))
1151 self.assertEqual(nntplib._unparse_datetime(dt, False),
1152 (date_str, time_str))
1153 gives(1999, 6, 23, 13, 56, 24, "19990623", "135624")
1154 gives(2000, 6, 23, 13, 56, 24, "20000623", "135624")
1155 gives(2010, 6, 5, 1, 2, 3, "20100605", "010203")
1156 # 2) with a date
1157 def gives(y, M, d, date_str, time_str):
1158 dt = datetime.date(y, M, d)
1159 self.assertEqual(nntplib._unparse_datetime(dt),
1160 (date_str, time_str))
1161 self.assertEqual(nntplib._unparse_datetime(dt, False),
1162 (date_str, time_str))
1163 gives(1999, 6, 23, "19990623", "000000")
1164 gives(2000, 6, 23, "20000623", "000000")
1165 gives(2010, 6, 5, "20100605", "000000")
1166
1167 def test_unparse_datetime_legacy(self):
1168 # Test legacy mode (RFC 977)
1169 # 1) with a datetime
1170 def gives(y, M, d, h, m, s, date_str, time_str):
1171 dt = datetime.datetime(y, M, d, h, m, s)
1172 self.assertEqual(nntplib._unparse_datetime(dt, True),
1173 (date_str, time_str))
1174 gives(1999, 6, 23, 13, 56, 24, "990623", "135624")
1175 gives(2000, 6, 23, 13, 56, 24, "000623", "135624")
1176 gives(2010, 6, 5, 1, 2, 3, "100605", "010203")
1177 # 2) with a date
1178 def gives(y, M, d, date_str, time_str):
1179 dt = datetime.date(y, M, d)
1180 self.assertEqual(nntplib._unparse_datetime(dt, True),
1181 (date_str, time_str))
1182 gives(1999, 6, 23, "990623", "000000")
1183 gives(2000, 6, 23, "000623", "000000")
1184 gives(2010, 6, 5, "100605", "000000")
1185
1186
1187def test_main():
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001188 tests = [MiscTests, NNTPv1Tests, NNTPv2Tests, NetworkedNNTPTests]
1189 if _have_ssl:
1190 tests.append(NetworkedNNTP_SSLTests)
1191 support.run_unittest(*tests)
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001192
1193
1194if __name__ == "__main__":
1195 test_main()