tests.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946
  1. # -*- coding: utf-8 -*-
  2. from __future__ import print_function
  3. import datetime
  4. import dateutil
  5. import re
  6. import sys
  7. import unittest
  8. import json
  9. from dateutil.tz import tzutc
  10. from dateutil.rrule import rrule, rruleset, WEEKLY, MONTHLY
  11. from vobject import base, iCalendar
  12. from vobject import icalendar
  13. from vobject.base import __behaviorRegistry as behavior_registry
  14. from vobject.base import ContentLine, parseLine, ParseError
  15. from vobject.base import readComponents, textLineToContentLine
  16. from vobject.change_tz import change_tz
  17. from vobject.icalendar import MultiDateBehavior, PeriodBehavior, \
  18. RecurringComponent, utc
  19. from vobject.icalendar import parseDtstart, stringToTextValues, \
  20. stringToPeriod, timedeltaToString
  21. two_hours = datetime.timedelta(hours=2)
  22. def get_test_file(path):
  23. """
  24. Helper function to open and read test files.
  25. """
  26. filepath = "test_files/{}".format(path)
  27. if sys.version_info[0] < 3:
  28. # On python 2, this library operates on bytes.
  29. f = open(filepath, 'r')
  30. else:
  31. # On python 3, it operates on unicode. We need to specify an encoding
  32. # for systems for which the preferred encoding isn't utf-8 (e.g windows)
  33. f = open(filepath, 'r', encoding='utf-8')
  34. text = f.read()
  35. f.close()
  36. return text
  37. class TestCalendarSerializing(unittest.TestCase):
  38. """
  39. Test creating an iCalendar file
  40. """
  41. max_diff = None
  42. def test_scratchbuild(self):
  43. """
  44. CreateCalendar 2.0 format from scratch
  45. """
  46. test_cal = get_test_file("simple_2_0_test.ics")
  47. cal = base.newFromBehavior('vcalendar', '2.0')
  48. cal.add('vevent')
  49. cal.vevent.add('dtstart').value = datetime.datetime(2006, 5, 9)
  50. cal.vevent.add('description').value = "Test event"
  51. cal.vevent.add('created').value = \
  52. datetime.datetime(2006, 1, 1, 10,
  53. tzinfo=dateutil.tz.tzical(
  54. "test_files/timezones.ics").get('US/Pacific'))
  55. cal.vevent.add('uid').value = "Not very random UID"
  56. cal.vevent.add('dtstamp').value = datetime.datetime(2017, 6, 26, 0, tzinfo=tzutc())
  57. cal.vevent.add('attendee').value = 'mailto:froelich@example.com'
  58. cal.vevent.attendee.params['CN'] = ['Fröhlich']
  59. # Note we're normalizing line endings, because no one got time for that.
  60. self.assertEqual(
  61. cal.serialize().replace('\r\n', '\n'),
  62. test_cal.replace('\r\n', '\n')
  63. )
  64. def test_unicode(self):
  65. """
  66. Test unicode characters
  67. """
  68. test_cal = get_test_file("utf8_test.ics")
  69. vevent = base.readOne(test_cal).vevent
  70. vevent2 = base.readOne(vevent.serialize())
  71. self.assertEqual(str(vevent), str(vevent2))
  72. self.assertEqual(
  73. vevent.summary.value,
  74. 'The title こんにちはキティ'
  75. )
  76. if sys.version_info[0] < 3:
  77. test_cal = test_cal.decode('utf-8')
  78. vevent = base.readOne(test_cal).vevent
  79. vevent2 = base.readOne(vevent.serialize())
  80. self.assertEqual(str(vevent), str(vevent2))
  81. self.assertEqual(
  82. vevent.summary.value,
  83. u'The title こんにちはキティ'
  84. )
  85. def test_wrapping(self):
  86. """
  87. Should support input file with a long text field covering multiple lines
  88. """
  89. test_journal = get_test_file("journal.ics")
  90. vobj = base.readOne(test_journal)
  91. vjournal = base.readOne(vobj.serialize())
  92. self.assertTrue('Joe, Lisa, and Bob' in vjournal.description.value)
  93. self.assertTrue('Tuesday.\n2.' in vjournal.description.value)
  94. def test_multiline(self):
  95. """
  96. Multi-text serialization test
  97. """
  98. category = base.newFromBehavior('categories')
  99. category.value = ['Random category']
  100. self.assertEqual(
  101. category.serialize().strip(),
  102. "CATEGORIES:Random category"
  103. )
  104. category.value.append('Other category')
  105. self.assertEqual(
  106. category.serialize().strip(),
  107. "CATEGORIES:Random category,Other category"
  108. )
  109. def test_semicolon_separated(self):
  110. """
  111. Semi-colon separated multi-text serialization test
  112. """
  113. request_status = base.newFromBehavior('request-status')
  114. request_status.value = ['5.1', 'Service unavailable']
  115. self.assertEqual(
  116. request_status.serialize().strip(),
  117. "REQUEST-STATUS:5.1;Service unavailable"
  118. )
  119. @staticmethod
  120. def test_unicode_multiline():
  121. """
  122. Test multiline unicode characters
  123. """
  124. cal = iCalendar()
  125. cal.add('method').value = 'REQUEST'
  126. cal.add('vevent')
  127. cal.vevent.add('created').value = datetime.datetime.now()
  128. cal.vevent.add('summary').value = 'Классное событие'
  129. cal.vevent.add('description').value = ('Классное событие Классное событие Классное событие Классное событие '
  130. 'Классное событие Классsdssdное событие')
  131. # json tries to encode as utf-8 and it would break if some chars could not be encoded
  132. json.dumps(cal.serialize())
  133. @staticmethod
  134. def test_ical_to_hcal():
  135. """
  136. Serializing iCalendar to hCalendar.
  137. Since Hcalendar is experimental and the behavior doesn't seem to want to load,
  138. This test will have to wait.
  139. tzs = dateutil.tz.tzical("test_files/timezones.ics")
  140. cal = base.newFromBehavior('hcalendar')
  141. self.assertEqual(
  142. str(cal.behavior),
  143. "<class 'vobject.hcalendar.HCalendar'>"
  144. )
  145. cal.add('vevent')
  146. cal.vevent.add('summary').value = "this is a note"
  147. cal.vevent.add('url').value = "http://microformats.org/code/hcalendar/creator"
  148. cal.vevent.add('dtstart').value = datetime.date(2006,2,27)
  149. cal.vevent.add('location').value = "a place"
  150. cal.vevent.add('dtend').value = datetime.date(2006,2,27) + datetime.timedelta(days = 2)
  151. event2 = cal.add('vevent')
  152. event2.add('summary').value = "Another one"
  153. event2.add('description').value = "The greatest thing ever!"
  154. event2.add('dtstart').value = datetime.datetime(1998, 12, 17, 16, 42, tzinfo = tzs.get('US/Pacific'))
  155. event2.add('location').value = "somewhere else"
  156. event2.add('dtend').value = event2.dtstart.value + datetime.timedelta(days = 6)
  157. hcal = cal.serialize()
  158. """
  159. #self.assertEqual(
  160. # str(hcal),
  161. # """<span class="vevent">
  162. # <a class="url" href="http://microformats.org/code/hcalendar/creator">
  163. # <span class="summary">this is a note</span>:
  164. # <abbr class="dtstart", title="20060227">Monday, February 27</abbr>
  165. # - <abbr class="dtend", title="20060301">Tuesday, February 28</abbr>
  166. # at <span class="location">a place</span>
  167. # </a>
  168. # </span>
  169. # <span class="vevent">
  170. # <span class="summary">Another one</span>:
  171. # <abbr class="dtstart", title="19981217T164200-0800">Thursday, December 17, 16:42</abbr>
  172. # - <abbr class="dtend", title="19981223T164200-0800">Wednesday, December 23, 16:42</abbr>
  173. # at <span class="location">somewhere else</span>
  174. # <div class="description">The greatest thing ever!</div>
  175. # </span>
  176. # """
  177. #)
  178. class TestBehaviors(unittest.TestCase):
  179. """
  180. Test Behaviors
  181. """
  182. def test_general_behavior(self):
  183. """
  184. Tests for behavior registry, getting and creating a behavior.
  185. """
  186. # Check expected behavior registry.
  187. self.assertEqual(
  188. sorted(behavior_registry.keys()),
  189. ['', 'ACTION', 'ADR', 'AVAILABLE', 'BUSYTYPE', 'CALSCALE',
  190. 'CATEGORIES', 'CLASS', 'COMMENT', 'COMPLETED', 'CONTACT',
  191. 'CREATED', 'DAYLIGHT', 'DESCRIPTION', 'DTEND', 'DTSTAMP',
  192. 'DTSTART', 'DUE', 'DURATION', 'EXDATE', 'EXRULE', 'FN', 'FREEBUSY',
  193. 'LABEL', 'LAST-MODIFIED', 'LOCATION', 'METHOD', 'N', 'ORG',
  194. 'PHOTO', 'PRODID', 'RDATE', 'RECURRENCE-ID', 'RELATED-TO',
  195. 'REQUEST-STATUS', 'RESOURCES', 'RRULE', 'STANDARD', 'STATUS',
  196. 'SUMMARY', 'TRANSP', 'TRIGGER', 'UID', 'VALARM', 'VAVAILABILITY',
  197. 'VCALENDAR', 'VCARD', 'VEVENT', 'VFREEBUSY', 'VJOURNAL',
  198. 'VTIMEZONE', 'VTODO']
  199. )
  200. # test get_behavior
  201. behavior = base.getBehavior('VCALENDAR')
  202. self.assertEqual(
  203. str(behavior),
  204. "<class 'vobject.icalendar.VCalendar2_0'>"
  205. )
  206. self.assertTrue(behavior.isComponent)
  207. self.assertEqual(
  208. base.getBehavior("invalid_name"),
  209. None
  210. )
  211. # test for ContentLine (not a component)
  212. non_component_behavior = base.getBehavior('RDATE')
  213. self.assertFalse(non_component_behavior.isComponent)
  214. def test_MultiDateBehavior(self):
  215. """
  216. Test MultiDateBehavior
  217. """
  218. parseRDate = MultiDateBehavior.transformToNative
  219. self.assertEqual(
  220. str(parseRDate(textLineToContentLine("RDATE;VALUE=DATE:19970304,19970504,19970704,19970904"))),
  221. "<RDATE{'VALUE': ['DATE']}[datetime.date(1997, 3, 4), datetime.date(1997, 5, 4), datetime.date(1997, 7, 4), datetime.date(1997, 9, 4)]>"
  222. )
  223. self.assertEqual(
  224. str(parseRDate(textLineToContentLine("RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H"))),
  225. "<RDATE{'VALUE': ['PERIOD']}[(datetime.datetime(1996, 4, 3, 2, 0, tzinfo=tzutc()), datetime.datetime(1996, 4, 3, 4, 0, tzinfo=tzutc())), (datetime.datetime(1996, 4, 4, 1, 0, tzinfo=tzutc()), " + ("datetime.timedelta(0, 10800)" if sys.version_info < (3,7) else "datetime.timedelta(seconds=10800)") + ")]>"
  226. )
  227. def test_periodBehavior(self):
  228. """
  229. Test PeriodBehavior
  230. """
  231. line = ContentLine('test', [], '', isNative=True)
  232. line.behavior = PeriodBehavior
  233. line.value = [(datetime.datetime(2006, 2, 16, 10), two_hours)]
  234. self.assertEqual(
  235. line.transformFromNative().value,
  236. '20060216T100000/PT2H'
  237. )
  238. self.assertEqual(
  239. line.transformToNative().value,
  240. [(datetime.datetime(2006, 2, 16, 10, 0),
  241. datetime.timedelta(0, 7200))]
  242. )
  243. line.value.append((datetime.datetime(2006, 5, 16, 10), two_hours))
  244. self.assertEqual(
  245. line.serialize().strip(),
  246. 'TEST:20060216T100000/PT2H,20060516T100000/PT2H'
  247. )
  248. class TestVTodo(unittest.TestCase):
  249. """
  250. VTodo Tests
  251. """
  252. def test_vtodo(self):
  253. """
  254. Test VTodo
  255. """
  256. vtodo = get_test_file("vtodo.ics")
  257. obj = base.readOne(vtodo)
  258. obj.vtodo.add('completed')
  259. obj.vtodo.completed.value = datetime.datetime(2015,5,5,13,30)
  260. self.assertEqual(obj.vtodo.completed.serialize()[0:23],
  261. 'COMPLETED:20150505T1330')
  262. obj = base.readOne(obj.serialize())
  263. self.assertEqual(obj.vtodo.completed.value,
  264. datetime.datetime(2015,5,5,13,30))
  265. class TestVobject(unittest.TestCase):
  266. """
  267. VObject Tests
  268. """
  269. max_diff = None
  270. @classmethod
  271. def setUpClass(cls):
  272. """
  273. Method for setting up class fixture before running tests in the class.
  274. Fetches test file.
  275. """
  276. cls.simple_test_cal = get_test_file("simple_test.ics")
  277. def test_readComponents(self):
  278. """
  279. Test if reading components correctly
  280. """
  281. cal = next(readComponents(self.simple_test_cal))
  282. self.assertEqual(str(cal), "<VCALENDAR| [<VEVENT| [<SUMMARY{'BLAH': ['hi!']}Bastille Day Party>]>]>")
  283. self.assertEqual(str(cal.vevent.summary), "<SUMMARY{'BLAH': ['hi!']}Bastille Day Party>")
  284. def test_parseLine(self):
  285. """
  286. Test line parsing
  287. """
  288. self.assertEqual(parseLine("BLAH:"), ('BLAH', [], '', None))
  289. self.assertEqual(
  290. parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904"),
  291. ('RDATE', [], 'VALUE=DATE:19970304,19970504,19970704,19970904', None)
  292. )
  293. self.assertEqual(
  294. parseLine('DESCRIPTION;ALTREP="http://www.wiz.org":The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA'),
  295. ('DESCRIPTION', [['ALTREP', 'http://www.wiz.org']], 'The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA', None)
  296. )
  297. self.assertEqual(
  298. parseLine("EMAIL;PREF;INTERNET:john@nowhere.com"),
  299. ('EMAIL', [['PREF'], ['INTERNET']], 'john@nowhere.com', None)
  300. )
  301. self.assertEqual(
  302. parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com'),
  303. ('EMAIL', [['TYPE', 'blah', 'hah'], ['INTERNET', 'DIGI', 'DERIDOO']], 'john@nowhere.com', None)
  304. )
  305. self.assertEqual(
  306. parseLine('item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;'),
  307. ('ADR', [['type', 'HOME'], ['type', 'pref']], ';;Reeperbahn 116;Hamburg;;20359;', 'item1')
  308. )
  309. self.assertRaises(ParseError, parseLine, ":")
  310. class TestGeneralFileParsing(unittest.TestCase):
  311. """
  312. General tests for parsing ics files.
  313. """
  314. def test_readOne(self):
  315. """
  316. Test reading first component of ics
  317. """
  318. cal = get_test_file("silly_test.ics")
  319. silly = base.readOne(cal)
  320. self.assertEqual(
  321. str(silly),
  322. "<SILLYPROFILE| [<MORESTUFF{}this line is not folded, but in practice probably ought to be, as it is exceptionally long, and moreover demonstratively stupid>, <SILLYNAME{}name>, <STUFF{}foldedline>]>"
  323. )
  324. self.assertEqual(
  325. str(silly.stuff),
  326. "<STUFF{}foldedline>"
  327. )
  328. def test_importing(self):
  329. """
  330. Test importing ics
  331. """
  332. cal = get_test_file("standard_test.ics")
  333. c = base.readOne(cal, validate=True)
  334. self.assertEqual(
  335. str(c.vevent.valarm.trigger),
  336. "<TRIGGER{}-1 day, 0:00:00>"
  337. )
  338. self.assertEqual(
  339. str(c.vevent.dtstart.value),
  340. "2002-10-28 14:00:00-08:00"
  341. )
  342. self.assertTrue(
  343. isinstance(c.vevent.dtstart.value, datetime.datetime)
  344. )
  345. self.assertEqual(
  346. str(c.vevent.dtend.value),
  347. "2002-10-28 15:00:00-08:00"
  348. )
  349. self.assertTrue(
  350. isinstance(c.vevent.dtend.value, datetime.datetime)
  351. )
  352. self.assertEqual(
  353. c.vevent.dtstamp.value,
  354. datetime.datetime(2002, 10, 28, 1, 17, 6, tzinfo=tzutc())
  355. )
  356. vevent = c.vevent.transformFromNative()
  357. self.assertEqual(
  358. str(vevent.rrule),
  359. "<RRULE{}FREQ=Weekly;COUNT=10>"
  360. )
  361. def test_bad_stream(self):
  362. """
  363. Test bad ics stream
  364. """
  365. cal = get_test_file("badstream.ics")
  366. self.assertRaises(ParseError, base.readOne, cal)
  367. def test_bad_line(self):
  368. """
  369. Test bad line in ics file
  370. """
  371. cal = get_test_file("badline.ics")
  372. self.assertRaises(ParseError, base.readOne, cal)
  373. newcal = base.readOne(cal, ignoreUnreadable=True)
  374. self.assertEqual(
  375. str(newcal.vevent.x_bad_underscore),
  376. '<X-BAD-UNDERSCORE{}TRUE>'
  377. )
  378. def test_parseParams(self):
  379. """
  380. Test parsing parameters
  381. """
  382. self.assertEqual(
  383. base.parseParams(';ALTREP="http://www.wiz.org"'),
  384. [['ALTREP', 'http://www.wiz.org']]
  385. )
  386. self.assertEqual(
  387. base.parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR'),
  388. [['ALTREP', 'http://www.wiz.org;;', 'Blah', 'Foo'],
  389. ['NEXT', 'Nope'], ['BAR']]
  390. )
  391. class TestVcards(unittest.TestCase):
  392. """
  393. Test VCards
  394. """
  395. @classmethod
  396. def setUpClass(cls):
  397. """
  398. Method for setting up class fixture before running tests in the class.
  399. Fetches test file.
  400. """
  401. cls.test_file = get_test_file("vcard_with_groups.ics")
  402. cls.card = base.readOne(cls.test_file)
  403. def test_vcard_creation(self):
  404. """
  405. Test creating a vCard
  406. """
  407. vcard = base.newFromBehavior('vcard', '3.0')
  408. self.assertEqual(
  409. str(vcard),
  410. "<VCARD| []>"
  411. )
  412. def test_default_behavior(self):
  413. """
  414. Default behavior test.
  415. """
  416. card = self.card
  417. self.assertEqual(
  418. base.getBehavior('note'),
  419. None
  420. )
  421. self.assertEqual(
  422. str(card.note.value),
  423. "The Mayor of the great city of Goerlitz in the great country of Germany.\nNext line."
  424. )
  425. def test_with_groups(self):
  426. """
  427. vCard groups test
  428. """
  429. card = self.card
  430. self.assertEqual(
  431. str(card.group),
  432. 'home'
  433. )
  434. self.assertEqual(
  435. str(card.tel.group),
  436. 'home'
  437. )
  438. card.group = card.tel.group = 'new'
  439. self.assertEqual(
  440. str(card.tel.serialize().strip()),
  441. 'new.TEL;TYPE=fax,voice,msg:+49 3581 123456'
  442. )
  443. self.assertEqual(
  444. str(card.serialize().splitlines()[0]),
  445. 'new.BEGIN:VCARD'
  446. )
  447. def test_vcard_3_parsing(self):
  448. """
  449. VCARD 3.0 parse test
  450. """
  451. test_file = get_test_file("simple_3_0_test.ics")
  452. card = base.readOne(test_file)
  453. # value not rendering correctly?
  454. #self.assertEqual(
  455. # card.adr.value,
  456. # "<Address: Haight Street 512;\nEscape, Test\nNovosibirsk, 80214\nGnuland>"
  457. #)
  458. self.assertEqual(
  459. card.org.value,
  460. ["University of Novosibirsk", "Department of Octopus Parthenogenesis"]
  461. )
  462. for _ in range(3):
  463. new_card = base.readOne(card.serialize())
  464. self.assertEqual(new_card.org.value, card.org.value)
  465. card = new_card
  466. class TestIcalendar(unittest.TestCase):
  467. """
  468. Tests for icalendar.py
  469. """
  470. max_diff = None
  471. def test_parseDTStart(self):
  472. """
  473. Should take a content line and return a datetime object.
  474. """
  475. self.assertEqual(
  476. parseDtstart(textLineToContentLine("DTSTART:20060509T000000")),
  477. datetime.datetime(2006, 5, 9, 0, 0)
  478. )
  479. def test_regexes(self):
  480. """
  481. Test regex patterns
  482. """
  483. self.assertEqual(
  484. re.findall(base.patterns['name'], '12foo-bar:yay'),
  485. ['12foo-bar', 'yay']
  486. )
  487. self.assertEqual(
  488. re.findall(base.patterns['safe_char'], 'a;b"*,cd'),
  489. ['a', 'b', '*', 'c', 'd']
  490. )
  491. self.assertEqual(
  492. re.findall(base.patterns['qsafe_char'], 'a;b"*,cd'),
  493. ['a', ';', 'b', '*', ',', 'c', 'd']
  494. )
  495. self.assertEqual(
  496. re.findall(base.patterns['param_value'],
  497. '"quoted";not-quoted;start"after-illegal-quote',
  498. re.VERBOSE),
  499. ['"quoted"', '', 'not-quoted', '', 'start', '',
  500. 'after-illegal-quote', '']
  501. )
  502. match = base.line_re.match('TEST;ALTREP="http://www.wiz.org":value:;"')
  503. self.assertEqual(
  504. match.group('value'),
  505. 'value:;"'
  506. )
  507. self.assertEqual(
  508. match.group('name'),
  509. 'TEST'
  510. )
  511. self.assertEqual(
  512. match.group('params'),
  513. ';ALTREP="http://www.wiz.org"'
  514. )
  515. def test_stringToTextValues(self):
  516. """
  517. Test string lists
  518. """
  519. self.assertEqual(
  520. stringToTextValues(''),
  521. ['']
  522. )
  523. self.assertEqual(
  524. stringToTextValues('abcd,efgh'),
  525. ['abcd', 'efgh']
  526. )
  527. def test_stringToPeriod(self):
  528. """
  529. Test datetime strings
  530. """
  531. self.assertEqual(
  532. stringToPeriod("19970101T180000Z/19970102T070000Z"),
  533. (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()),
  534. datetime.datetime(1997, 1, 2, 7, 0, tzinfo=tzutc()))
  535. )
  536. self.assertEqual(
  537. stringToPeriod("19970101T180000Z/PT1H"),
  538. (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()),
  539. datetime.timedelta(0, 3600))
  540. )
  541. def test_timedeltaToString(self):
  542. """
  543. Test timedelta strings
  544. """
  545. self.assertEqual(
  546. timedeltaToString(two_hours),
  547. 'PT2H'
  548. )
  549. self.assertEqual(
  550. timedeltaToString(datetime.timedelta(minutes=20)),
  551. 'PT20M'
  552. )
  553. def test_vtimezone_creation(self):
  554. """
  555. Test timezones
  556. """
  557. tzs = dateutil.tz.tzical("test_files/timezones.ics")
  558. pacific = icalendar.TimezoneComponent(tzs.get('US/Pacific'))
  559. self.assertEqual(
  560. str(pacific),
  561. "<VTIMEZONE | <TZID{}US/Pacific>>"
  562. )
  563. santiago = icalendar.TimezoneComponent(tzs.get('Santiago'))
  564. self.assertEqual(
  565. str(santiago),
  566. "<VTIMEZONE | <TZID{}Santiago>>"
  567. )
  568. for year in range(2001, 2010):
  569. for month in (2, 9):
  570. dt = datetime.datetime(year, month, 15,
  571. tzinfo=tzs.get('Santiago'))
  572. self.assertTrue(dt.replace(tzinfo=tzs.get('Santiago')), dt)
  573. @staticmethod
  574. def test_timezone_serializing():
  575. """
  576. Serializing with timezones test
  577. """
  578. tzs = dateutil.tz.tzical("test_files/timezones.ics")
  579. pacific = tzs.get('US/Pacific')
  580. cal = base.Component('VCALENDAR')
  581. cal.setBehavior(icalendar.VCalendar2_0)
  582. ev = cal.add('vevent')
  583. ev.add('dtstart').value = datetime.datetime(2005, 10, 12, 9,
  584. tzinfo=pacific)
  585. evruleset = rruleset()
  586. evruleset.rrule(rrule(WEEKLY, interval=2, byweekday=[2,4],
  587. until=datetime.datetime(2005, 12, 15, 9)))
  588. evruleset.rrule(rrule(MONTHLY, bymonthday=[-1,-5]))
  589. evruleset.exdate(datetime.datetime(2005, 10, 14, 9, tzinfo=pacific))
  590. ev.rruleset = evruleset
  591. ev.add('duration').value = datetime.timedelta(hours=1)
  592. apple = tzs.get('America/Montreal')
  593. ev.dtstart.value = datetime.datetime(2005, 10, 12, 9, tzinfo=apple)
  594. def test_pytz_timezone_serializing(self):
  595. """
  596. Serializing with timezones from pytz test
  597. """
  598. try:
  599. import pytz
  600. except ImportError:
  601. return self.skipTest("pytz not installed") # NOQA
  602. # Avoid conflicting cached tzinfo from other tests
  603. def unregister_tzid(tzid):
  604. """Clear tzid from icalendar TZID registry"""
  605. if icalendar.getTzid(tzid, False):
  606. icalendar.registerTzid(tzid, None)
  607. unregister_tzid('US/Eastern')
  608. eastern = pytz.timezone('US/Eastern')
  609. cal = base.Component('VCALENDAR')
  610. cal.setBehavior(icalendar.VCalendar2_0)
  611. ev = cal.add('vevent')
  612. ev.add('dtstart').value = eastern.localize(
  613. datetime.datetime(2008, 10, 12, 9))
  614. serialized = cal.serialize()
  615. expected_vtimezone = get_test_file("tz_us_eastern.ics")
  616. self.assertIn(
  617. expected_vtimezone.replace('\r\n', '\n'),
  618. serialized.replace('\r\n', '\n')
  619. )
  620. # Exhaustively test all zones (just looking for no errors)
  621. for tzname in pytz.all_timezones:
  622. unregister_tzid(tzname)
  623. tz = icalendar.TimezoneComponent(tzinfo=pytz.timezone(tzname))
  624. tz.serialize()
  625. def test_freeBusy(self):
  626. """
  627. Test freebusy components
  628. """
  629. test_cal = get_test_file("freebusy.ics")
  630. vfb = base.newFromBehavior('VFREEBUSY')
  631. vfb.add('uid').value = 'test'
  632. vfb.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc)
  633. vfb.add('dtstart').value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc)
  634. vfb.add('dtend').value = vfb.dtstart.value + two_hours
  635. vfb.add('freebusy').value = [(vfb.dtstart.value, two_hours / 2)]
  636. vfb.add('freebusy').value = [(vfb.dtstart.value, vfb.dtend.value)]
  637. self.assertEqual(
  638. vfb.serialize().replace('\r\n', '\n'),
  639. test_cal.replace('\r\n', '\n')
  640. )
  641. def test_availablity(self):
  642. """
  643. Test availability components
  644. """
  645. test_cal = get_test_file("availablity.ics")
  646. vcal = base.newFromBehavior('VAVAILABILITY')
  647. vcal.add('uid').value = 'test'
  648. vcal.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc)
  649. vcal.add('dtstart').value = datetime.datetime(2006, 2, 16, 0, tzinfo=utc)
  650. vcal.add('dtend').value = datetime.datetime(2006, 2, 17, 0, tzinfo=utc)
  651. vcal.add('busytype').value = "BUSY"
  652. av = base.newFromBehavior('AVAILABLE')
  653. av.add('uid').value = 'test1'
  654. av.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc)
  655. av.add('dtstart').value = datetime.datetime(2006, 2, 16, 9, tzinfo=utc)
  656. av.add('dtend').value = datetime.datetime(2006, 2, 16, 12, tzinfo=utc)
  657. av.add('summary').value = "Available in the morning"
  658. vcal.add(av)
  659. self.assertEqual(
  660. vcal.serialize().replace('\r\n', '\n'),
  661. test_cal.replace('\r\n', '\n')
  662. )
  663. def test_recurrence(self):
  664. """
  665. Ensure date valued UNTILs in rrules are in a reasonable timezone,
  666. and include that day (12/28 in this test)
  667. """
  668. test_file = get_test_file("recurrence.ics")
  669. cal = base.readOne(test_file)
  670. dates = list(cal.vevent.getrruleset())
  671. self.assertEqual(
  672. dates[0],
  673. datetime.datetime(2006, 1, 26, 23, 0, tzinfo=tzutc())
  674. )
  675. self.assertEqual(
  676. dates[1],
  677. datetime.datetime(2006, 2, 23, 23, 0, tzinfo=tzutc())
  678. )
  679. self.assertEqual(
  680. dates[-1],
  681. datetime.datetime(2006, 12, 28, 23, 0, tzinfo=tzutc())
  682. )
  683. def test_recurring_component(self):
  684. """
  685. Test recurring events
  686. """
  687. vevent = RecurringComponent(name='VEVENT')
  688. # init
  689. self.assertTrue(vevent.isNative)
  690. # rruleset should be None at this point.
  691. # No rules have been passed or created.
  692. self.assertEqual(vevent.rruleset, None)
  693. # Now add start and rule for recurring event
  694. vevent.add('dtstart').value = datetime.datetime(2005, 1, 19, 9)
  695. vevent.add('rrule').value =u"FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH"
  696. self.assertEqual(
  697. list(vevent.rruleset),
  698. [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)]
  699. )
  700. self.assertEqual(
  701. list(vevent.getrruleset(addRDate=True)),
  702. [datetime.datetime(2005, 1, 19, 9, 0), datetime.datetime(2005, 1, 20, 9, 0)]
  703. )
  704. # Also note that dateutil will expand all-day events (datetime.date values)
  705. # to datetime.datetime value with time 0 and no timezone.
  706. vevent.dtstart.value = datetime.date(2005,3,18)
  707. self.assertEqual(
  708. list(vevent.rruleset),
  709. [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)]
  710. )
  711. self.assertEqual(
  712. list(vevent.getrruleset(True)),
  713. [datetime.datetime(2005, 3, 18, 0, 0), datetime.datetime(2005, 3, 29, 0, 0)]
  714. )
  715. def test_recurrence_without_tz(self):
  716. """
  717. Test recurring vevent missing any time zone definitions.
  718. """
  719. test_file = get_test_file("recurrence-without-tz.ics")
  720. cal = base.readOne(test_file)
  721. dates = list(cal.vevent.getrruleset())
  722. self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0))
  723. self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0))
  724. self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0))
  725. def test_recurrence_offset_naive(self):
  726. """
  727. Ensure recurring vevent missing some time zone definitions is
  728. parsing. See isseu #75.
  729. """
  730. test_file = get_test_file("recurrence-offset-naive.ics")
  731. cal = base.readOne(test_file)
  732. dates = list(cal.vevent.getrruleset())
  733. self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0))
  734. self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0))
  735. self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0))
  736. class TestChangeTZ(unittest.TestCase):
  737. """
  738. Tests for change_tz.change_tz
  739. """
  740. class StubCal(object):
  741. class StubEvent(object):
  742. class Node(object):
  743. def __init__(self, value):
  744. self.value = value
  745. def __init__(self, dtstart, dtend):
  746. self.dtstart = self.Node(dtstart)
  747. self.dtend = self.Node(dtend)
  748. def __init__(self, dates):
  749. """
  750. dates is a list of tuples (dtstart, dtend)
  751. """
  752. self.vevent_list = [self.StubEvent(*d) for d in dates]
  753. def test_change_tz(self):
  754. """
  755. Change the timezones of events in a component to a different
  756. timezone
  757. """
  758. # Setup - create a stub vevent list
  759. old_tz = dateutil.tz.gettz('UTC') # 0:00
  760. new_tz = dateutil.tz.gettz('America/Chicago') # -5:00
  761. dates = [
  762. (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=old_tz),
  763. datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=old_tz)),
  764. (datetime.datetime(2010, 12, 31, 23, 59, 59, 0, tzinfo=old_tz),
  765. datetime.datetime(2011, 1, 2, 3, 0, 0, 0, tzinfo=old_tz))]
  766. cal = self.StubCal(dates)
  767. # Exercise - change the timezone
  768. change_tz(cal, new_tz, dateutil.tz.gettz('UTC'))
  769. # Test - that the tzs were converted correctly
  770. expected_new_dates = [
  771. (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz),
  772. datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz)),
  773. (datetime.datetime(2010, 12, 31, 17, 59, 59, 0, tzinfo=new_tz),
  774. datetime.datetime(2011, 1, 1, 21, 0, 0, 0, tzinfo=new_tz))]
  775. for vevent, expected_datepair in zip(cal.vevent_list,
  776. expected_new_dates):
  777. self.assertEqual(vevent.dtstart.value, expected_datepair[0])
  778. self.assertEqual(vevent.dtend.value, expected_datepair[1])
  779. def test_change_tz_utc_only(self):
  780. """
  781. Change any UTC timezones of events in a component to a different
  782. timezone
  783. """
  784. # Setup - create a stub vevent list
  785. utc_tz = dateutil.tz.gettz('UTC') # 0:00
  786. non_utc_tz = dateutil.tz.gettz('America/Santiago') # -4:00
  787. new_tz = dateutil.tz.gettz('America/Chicago') # -5:00
  788. dates = [
  789. (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=utc_tz),
  790. datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=non_utc_tz))]
  791. cal = self.StubCal(dates)
  792. # Exercise - change the timezone passing utc_only=True
  793. change_tz(cal, new_tz, dateutil.tz.gettz('UTC'), utc_only=True)
  794. # Test - that only the utc item has changed
  795. expected_new_dates = [
  796. (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz),
  797. dates[0][1])]
  798. for vevent, expected_datepair in zip(cal.vevent_list,
  799. expected_new_dates):
  800. self.assertEqual(vevent.dtstart.value, expected_datepair[0])
  801. self.assertEqual(vevent.dtend.value, expected_datepair[1])
  802. def test_change_tz_default(self):
  803. """
  804. Change the timezones of events in a component to a different
  805. timezone, passing a default timezone that is assumed when the events
  806. don't have one
  807. """
  808. # Setup - create a stub vevent list
  809. new_tz = dateutil.tz.gettz('America/Chicago') # -5:00
  810. dates = [
  811. (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=None),
  812. datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=None))]
  813. cal = self.StubCal(dates)
  814. # Exercise - change the timezone
  815. change_tz(cal, new_tz, dateutil.tz.gettz('UTC'))
  816. # Test - that the tzs were converted correctly
  817. expected_new_dates = [
  818. (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz),
  819. datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz))]
  820. for vevent, expected_datepair in zip(cal.vevent_list,
  821. expected_new_dates):
  822. self.assertEqual(vevent.dtstart.value, expected_datepair[0])
  823. self.assertEqual(vevent.dtend.value, expected_datepair[1])
  824. if __name__ == '__main__':
  825. unittest.main()