test_base.py 34 KB

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