From d246a6c90e6a5aa597b841c6e3f2bba8b66fbb30 Mon Sep 17 00:00:00 2001 From: "adam.stursa" Date: Tue, 12 Jul 2022 14:59:04 +0200 Subject: [PATCH 1/2] Added jcard (json vcard) parsing functionality --- vobject/vcard.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/vobject/vcard.py b/vobject/vcard.py index 1941e82..d7302f8 100644 --- a/vobject/vcard.py +++ b/vobject/vcard.py @@ -1,10 +1,11 @@ """Definitions and behavior for vCard 3.0""" import codecs +import json from . import behavior -from .base import ContentLine, registerBehavior, backslashEscape, str_ +from .base import ContentLine, registerBehavior, backslashEscape, str_, newFromBehavior, ParseError from .icalendar import stringToTextValues @@ -368,3 +369,38 @@ def transformFromNative(obj): obj.value = serializeFields(obj.value) return obj registerBehavior(OrgBehavior, 'ORG') + + +def fromJCards(jcards): + if isinstance(jcards, str): + jcards = json.loads(jcards) + + if jcards[0] != "vcard" or len(jcards) < 2: + raise ParseError("Not a valid jcard object") + vcards = [] + # skip first one since it is "vcard" string (as defined in RFC 7095) + for jcard in jcards[1:]: + # first property MUST be version (as defined in RFC 7095) + version = jcard[0][3] + vcard = newFromBehavior('vcard', version) + # loop through properties except version and add them + for prop in jcard[1:]: + name, params, val_type, *value = prop + obj = vcard.add(name) + # set value corresponding to property + if name == "adr": + obj.value = Address(**dict(zip(ADDRESS_ORDER, value[0]))) + elif name == "org" or len(value) > 1: + obj.value = value + elif name == "n": + obj.value = Name(**dict(zip(NAME_ORDER, value[0]))) + else: + obj.value = value[0] + # add value param + obj.value_param = val_type + # add other params from json + for k, v in params.items(): + setattr(obj, k.lower()+"_param", v) + vcards.append(vcard) + + return vcards if len(vcards) > 1 else vcard From f47c265ac675206c837247ce8871b64cd1c05d47 Mon Sep 17 00:00:00 2001 From: "adam.stursa" Date: Tue, 12 Jul 2022 14:59:56 +0200 Subject: [PATCH 2/2] Tests for jcard functionality --- test_files/jcard.json | 114 +++ tests.py | 1904 +++++++++++++++++++++-------------------- 2 files changed, 1072 insertions(+), 946 deletions(-) create mode 100644 test_files/jcard.json diff --git a/test_files/jcard.json b/test_files/jcard.json new file mode 100644 index 0000000..ed8e942 --- /dev/null +++ b/test_files/jcard.json @@ -0,0 +1,114 @@ +[ + "vcard", + [ + [ + "version", + {}, + "text", + "3.0" + ], + [ + "fn", + {}, + "text", + "Simon Perreault" + ], + [ + "n", + {}, + "text", + [ + "Perreault", + "Simon", + "", + "", + [ + "ing. jr", + "M.Sc." + ] + ] + ], + [ + "org", + { + "type": "work" + }, + "text", + "Viagenie" + ], + [ + "adr", + { + "type": "work" + }, + "text", + [ + "", + "Suite D2-630", + "2875 Laurier", + "Quebec", + "QC", + "G1V 2M2", + "Canada" + ] + ], + [ + "tel", + { + "type": [ + "work", + "voice" + ], + "pref": "1" + }, + "uri", + "tel:+1-418-656-9254;ext=102" + ], + [ + "tel", + { + "type": [ + "work", + "cell", + "voice", + "video", + "text" + ] + }, + "uri", + "tel:+1-418-262-6501" + ], + [ + "email", + { + "type": "work" + }, + "text", + "simon.perreault@viagenie.ca" + ], + [ + "geo", + { + "type": "work" + }, + "uri", + "geo:46.772673,-71.282945" + ], + [ + "key", + { + "type": "work" + }, + "uri", + "http://www.viagenie.ca/simon.perreault/simon.asc" + ], + [ + "url", + { + "type": "home" + }, + "uri", + "http://nomis80.org" + ] + ] +] \ No newline at end of file diff --git a/tests.py b/tests.py index fcab478..d2afded 100644 --- a/tests.py +++ b/tests.py @@ -1,946 +1,958 @@ -# -*- coding: utf-8 -*- - -from __future__ import print_function - -import datetime -import dateutil -import re -import sys -import unittest -import json - -from dateutil.tz import tzutc -from dateutil.rrule import rrule, rruleset, WEEKLY, MONTHLY - -from vobject import base, iCalendar -from vobject import icalendar - -from vobject.base import __behaviorRegistry as behavior_registry -from vobject.base import ContentLine, parseLine, ParseError -from vobject.base import readComponents, textLineToContentLine - -from vobject.change_tz import change_tz - -from vobject.icalendar import MultiDateBehavior, PeriodBehavior, \ - RecurringComponent, utc -from vobject.icalendar import parseDtstart, stringToTextValues, \ - stringToPeriod, timedeltaToString - -two_hours = datetime.timedelta(hours=2) - - -def get_test_file(path): - """ - Helper function to open and read test files. - """ - filepath = "test_files/{}".format(path) - if sys.version_info[0] < 3: - # On python 2, this library operates on bytes. - f = open(filepath, 'r') - else: - # On python 3, it operates on unicode. We need to specify an encoding - # for systems for which the preferred encoding isn't utf-8 (e.g windows) - f = open(filepath, 'r', encoding='utf-8') - text = f.read() - f.close() - return text - - -class TestCalendarSerializing(unittest.TestCase): - """ - Test creating an iCalendar file - """ - max_diff = None - - def test_scratchbuild(self): - """ - CreateCalendar 2.0 format from scratch - """ - test_cal = get_test_file("simple_2_0_test.ics") - cal = base.newFromBehavior('vcalendar', '2.0') - cal.add('vevent') - cal.vevent.add('dtstart').value = datetime.datetime(2006, 5, 9) - cal.vevent.add('description').value = "Test event" - cal.vevent.add('created').value = \ - datetime.datetime(2006, 1, 1, 10, - tzinfo=dateutil.tz.tzical( - "test_files/timezones.ics").get('US/Pacific')) - cal.vevent.add('uid').value = "Not very random UID" - cal.vevent.add('dtstamp').value = datetime.datetime(2017, 6, 26, 0, tzinfo=tzutc()) - - cal.vevent.add('attendee').value = 'mailto:froelich@example.com' - cal.vevent.attendee.params['CN'] = ['Fröhlich'] - - # Note we're normalizing line endings, because no one got time for that. - self.assertEqual( - cal.serialize().replace('\r\n', '\n'), - test_cal.replace('\r\n', '\n') - ) - - def test_unicode(self): - """ - Test unicode characters - """ - test_cal = get_test_file("utf8_test.ics") - vevent = base.readOne(test_cal).vevent - vevent2 = base.readOne(vevent.serialize()) - self.assertEqual(str(vevent), str(vevent2)) - - self.assertEqual( - vevent.summary.value, - 'The title こんにちはキティ' - ) - - if sys.version_info[0] < 3: - test_cal = test_cal.decode('utf-8') - vevent = base.readOne(test_cal).vevent - vevent2 = base.readOne(vevent.serialize()) - self.assertEqual(str(vevent), str(vevent2)) - self.assertEqual( - vevent.summary.value, - u'The title こんにちはキティ' - ) - - def test_wrapping(self): - """ - Should support input file with a long text field covering multiple lines - """ - test_journal = get_test_file("journal.ics") - vobj = base.readOne(test_journal) - vjournal = base.readOne(vobj.serialize()) - self.assertTrue('Joe, Lisa, and Bob' in vjournal.description.value) - self.assertTrue('Tuesday.\n2.' in vjournal.description.value) - - def test_multiline(self): - """ - Multi-text serialization test - """ - category = base.newFromBehavior('categories') - category.value = ['Random category'] - self.assertEqual( - category.serialize().strip(), - "CATEGORIES:Random category" - ) - - category.value.append('Other category') - self.assertEqual( - category.serialize().strip(), - "CATEGORIES:Random category,Other category" - ) - - def test_semicolon_separated(self): - """ - Semi-colon separated multi-text serialization test - """ - request_status = base.newFromBehavior('request-status') - request_status.value = ['5.1', 'Service unavailable'] - self.assertEqual( - request_status.serialize().strip(), - "REQUEST-STATUS:5.1;Service unavailable" - ) - - @staticmethod - def test_unicode_multiline(): - """ - Test multiline unicode characters - """ - cal = iCalendar() - cal.add('method').value = 'REQUEST' - cal.add('vevent') - cal.vevent.add('created').value = datetime.datetime.now() - cal.vevent.add('summary').value = 'Классное событие' - cal.vevent.add('description').value = ('Классное событие Классное событие Классное событие Классное событие ' - 'Классное событие Классsdssdное событие') - - # json tries to encode as utf-8 and it would break if some chars could not be encoded - json.dumps(cal.serialize()) - - @staticmethod - def test_ical_to_hcal(): - """ - Serializing iCalendar to hCalendar. - - Since Hcalendar is experimental and the behavior doesn't seem to want to load, - This test will have to wait. - - - tzs = dateutil.tz.tzical("test_files/timezones.ics") - cal = base.newFromBehavior('hcalendar') - self.assertEqual( - str(cal.behavior), - "" - ) - cal.add('vevent') - cal.vevent.add('summary').value = "this is a note" - cal.vevent.add('url').value = "http://microformats.org/code/hcalendar/creator" - cal.vevent.add('dtstart').value = datetime.date(2006,2,27) - cal.vevent.add('location').value = "a place" - cal.vevent.add('dtend').value = datetime.date(2006,2,27) + datetime.timedelta(days = 2) - - event2 = cal.add('vevent') - event2.add('summary').value = "Another one" - event2.add('description').value = "The greatest thing ever!" - event2.add('dtstart').value = datetime.datetime(1998, 12, 17, 16, 42, tzinfo = tzs.get('US/Pacific')) - event2.add('location').value = "somewhere else" - event2.add('dtend').value = event2.dtstart.value + datetime.timedelta(days = 6) - hcal = cal.serialize() - """ - #self.assertEqual( - # str(hcal), - # """ - # - # this is a note: - # Monday, February 27 - # - Tuesday, February 28 - # at a place - # - # - # - # Another one: - # Thursday, December 17, 16:42 - # - Wednesday, December 23, 16:42 - # at somewhere else - #
The greatest thing ever!
- #
- # """ - #) - - -class TestBehaviors(unittest.TestCase): - """ - Test Behaviors - """ - def test_general_behavior(self): - """ - Tests for behavior registry, getting and creating a behavior. - """ - # Check expected behavior registry. - self.assertEqual( - sorted(behavior_registry.keys()), - ['', 'ACTION', 'ADR', 'AVAILABLE', 'BUSYTYPE', 'CALSCALE', - 'CATEGORIES', 'CLASS', 'COMMENT', 'COMPLETED', 'CONTACT', - 'CREATED', 'DAYLIGHT', 'DESCRIPTION', 'DTEND', 'DTSTAMP', - 'DTSTART', 'DUE', 'DURATION', 'EXDATE', 'EXRULE', 'FN', 'FREEBUSY', - 'LABEL', 'LAST-MODIFIED', 'LOCATION', 'METHOD', 'N', 'ORG', - 'PHOTO', 'PRODID', 'RDATE', 'RECURRENCE-ID', 'RELATED-TO', - 'REQUEST-STATUS', 'RESOURCES', 'RRULE', 'STANDARD', 'STATUS', - 'SUMMARY', 'TRANSP', 'TRIGGER', 'UID', 'VALARM', 'VAVAILABILITY', - 'VCALENDAR', 'VCARD', 'VEVENT', 'VFREEBUSY', 'VJOURNAL', - 'VTIMEZONE', 'VTODO'] - ) - - # test get_behavior - behavior = base.getBehavior('VCALENDAR') - self.assertEqual( - str(behavior), - "" - ) - self.assertTrue(behavior.isComponent) - - self.assertEqual( - base.getBehavior("invalid_name"), - None - ) - # test for ContentLine (not a component) - non_component_behavior = base.getBehavior('RDATE') - self.assertFalse(non_component_behavior.isComponent) - - def test_MultiDateBehavior(self): - """ - Test MultiDateBehavior - """ - parseRDate = MultiDateBehavior.transformToNative - self.assertEqual( - str(parseRDate(textLineToContentLine("RDATE;VALUE=DATE:19970304,19970504,19970704,19970904"))), - "" - ) - self.assertEqual( - str(parseRDate(textLineToContentLine("RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H"))), - "" - ) - - def test_periodBehavior(self): - """ - Test PeriodBehavior - """ - line = ContentLine('test', [], '', isNative=True) - line.behavior = PeriodBehavior - line.value = [(datetime.datetime(2006, 2, 16, 10), two_hours)] - - self.assertEqual( - line.transformFromNative().value, - '20060216T100000/PT2H' - ) - self.assertEqual( - line.transformToNative().value, - [(datetime.datetime(2006, 2, 16, 10, 0), - datetime.timedelta(0, 7200))] - ) - - line.value.append((datetime.datetime(2006, 5, 16, 10), two_hours)) - - self.assertEqual( - line.serialize().strip(), - 'TEST:20060216T100000/PT2H,20060516T100000/PT2H' - ) - - -class TestVTodo(unittest.TestCase): - """ - VTodo Tests - """ - def test_vtodo(self): - """ - Test VTodo - """ - vtodo = get_test_file("vtodo.ics") - obj = base.readOne(vtodo) - obj.vtodo.add('completed') - obj.vtodo.completed.value = datetime.datetime(2015,5,5,13,30) - self.assertEqual(obj.vtodo.completed.serialize()[0:23], - 'COMPLETED:20150505T1330') - obj = base.readOne(obj.serialize()) - self.assertEqual(obj.vtodo.completed.value, - datetime.datetime(2015,5,5,13,30)) - - -class TestVobject(unittest.TestCase): - """ - VObject Tests - """ - max_diff = None - - @classmethod - def setUpClass(cls): - """ - Method for setting up class fixture before running tests in the class. - Fetches test file. - """ - cls.simple_test_cal = get_test_file("simple_test.ics") - - def test_readComponents(self): - """ - Test if reading components correctly - """ - cal = next(readComponents(self.simple_test_cal)) - - self.assertEqual(str(cal), "]>]>") - self.assertEqual(str(cal.vevent.summary), "") - - def test_parseLine(self): - """ - Test line parsing - """ - self.assertEqual(parseLine("BLAH:"), ('BLAH', [], '', None)) - self.assertEqual( - parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904"), - ('RDATE', [], 'VALUE=DATE:19970304,19970504,19970704,19970904', None) - ) - self.assertEqual( - parseLine('DESCRIPTION;ALTREP="http://www.wiz.org":The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA'), - ('DESCRIPTION', [['ALTREP', 'http://www.wiz.org']], 'The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA', None) - ) - self.assertEqual( - parseLine("EMAIL;PREF;INTERNET:john@nowhere.com"), - ('EMAIL', [['PREF'], ['INTERNET']], 'john@nowhere.com', None) - ) - self.assertEqual( - parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com'), - ('EMAIL', [['TYPE', 'blah', 'hah'], ['INTERNET', 'DIGI', 'DERIDOO']], 'john@nowhere.com', None) - ) - self.assertEqual( - parseLine('item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;'), - ('ADR', [['type', 'HOME'], ['type', 'pref']], ';;Reeperbahn 116;Hamburg;;20359;', 'item1') - ) - self.assertRaises(ParseError, parseLine, ":") - - -class TestGeneralFileParsing(unittest.TestCase): - """ - General tests for parsing ics files. - """ - def test_readOne(self): - """ - Test reading first component of ics - """ - cal = get_test_file("silly_test.ics") - silly = base.readOne(cal) - self.assertEqual( - str(silly), - ", , ]>" - ) - self.assertEqual( - str(silly.stuff), - "" - ) - - def test_importing(self): - """ - Test importing ics - """ - cal = get_test_file("standard_test.ics") - c = base.readOne(cal, validate=True) - self.assertEqual( - str(c.vevent.valarm.trigger), - "" - ) - - self.assertEqual( - str(c.vevent.dtstart.value), - "2002-10-28 14:00:00-08:00" - ) - self.assertTrue( - isinstance(c.vevent.dtstart.value, datetime.datetime) - ) - self.assertEqual( - str(c.vevent.dtend.value), - "2002-10-28 15:00:00-08:00" - ) - self.assertTrue( - isinstance(c.vevent.dtend.value, datetime.datetime) - ) - self.assertEqual( - c.vevent.dtstamp.value, - datetime.datetime(2002, 10, 28, 1, 17, 6, tzinfo=tzutc()) - ) - - vevent = c.vevent.transformFromNative() - self.assertEqual( - str(vevent.rrule), - "" - ) - - def test_bad_stream(self): - """ - Test bad ics stream - """ - cal = get_test_file("badstream.ics") - self.assertRaises(ParseError, base.readOne, cal) - - def test_bad_line(self): - """ - Test bad line in ics file - """ - cal = get_test_file("badline.ics") - self.assertRaises(ParseError, base.readOne, cal) - - newcal = base.readOne(cal, ignoreUnreadable=True) - self.assertEqual( - str(newcal.vevent.x_bad_underscore), - '' - ) - - def test_parseParams(self): - """ - Test parsing parameters - """ - self.assertEqual( - base.parseParams(';ALTREP="http://www.wiz.org"'), - [['ALTREP', 'http://www.wiz.org']] - ) - self.assertEqual( - base.parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR'), - [['ALTREP', 'http://www.wiz.org;;', 'Blah', 'Foo'], - ['NEXT', 'Nope'], ['BAR']] - ) - - -class TestVcards(unittest.TestCase): - """ - Test VCards - """ - @classmethod - def setUpClass(cls): - """ - Method for setting up class fixture before running tests in the class. - Fetches test file. - """ - cls.test_file = get_test_file("vcard_with_groups.ics") - cls.card = base.readOne(cls.test_file) - - def test_vcard_creation(self): - """ - Test creating a vCard - """ - vcard = base.newFromBehavior('vcard', '3.0') - self.assertEqual( - str(vcard), - "" - ) - - def test_default_behavior(self): - """ - Default behavior test. - """ - card = self.card - self.assertEqual( - base.getBehavior('note'), - None - ) - self.assertEqual( - str(card.note.value), - "The Mayor of the great city of Goerlitz in the great country of Germany.\nNext line." - ) - - def test_with_groups(self): - """ - vCard groups test - """ - card = self.card - self.assertEqual( - str(card.group), - 'home' - ) - self.assertEqual( - str(card.tel.group), - 'home' - ) - - card.group = card.tel.group = 'new' - self.assertEqual( - str(card.tel.serialize().strip()), - 'new.TEL;TYPE=fax,voice,msg:+49 3581 123456' - ) - self.assertEqual( - str(card.serialize().splitlines()[0]), - 'new.BEGIN:VCARD' - ) - - - def test_vcard_3_parsing(self): - """ - VCARD 3.0 parse test - """ - test_file = get_test_file("simple_3_0_test.ics") - card = base.readOne(test_file) - # value not rendering correctly? - #self.assertEqual( - # card.adr.value, - # "" - #) - self.assertEqual( - card.org.value, - ["University of Novosibirsk", "Department of Octopus Parthenogenesis"] - ) - - for _ in range(3): - new_card = base.readOne(card.serialize()) - self.assertEqual(new_card.org.value, card.org.value) - card = new_card - - -class TestIcalendar(unittest.TestCase): - """ - Tests for icalendar.py - """ - max_diff = None - def test_parseDTStart(self): - """ - Should take a content line and return a datetime object. - """ - self.assertEqual( - parseDtstart(textLineToContentLine("DTSTART:20060509T000000")), - datetime.datetime(2006, 5, 9, 0, 0) - ) - - def test_regexes(self): - """ - Test regex patterns - """ - self.assertEqual( - re.findall(base.patterns['name'], '12foo-bar:yay'), - ['12foo-bar', 'yay'] - ) - self.assertEqual( - re.findall(base.patterns['safe_char'], 'a;b"*,cd'), - ['a', 'b', '*', 'c', 'd'] - ) - self.assertEqual( - re.findall(base.patterns['qsafe_char'], 'a;b"*,cd'), - ['a', ';', 'b', '*', ',', 'c', 'd'] - ) - self.assertEqual( - re.findall(base.patterns['param_value'], - '"quoted";not-quoted;start"after-illegal-quote', - re.VERBOSE), - ['"quoted"', '', 'not-quoted', '', 'start', '', - 'after-illegal-quote', ''] - ) - match = base.line_re.match('TEST;ALTREP="http://www.wiz.org":value:;"') - self.assertEqual( - match.group('value'), - 'value:;"' - ) - self.assertEqual( - match.group('name'), - 'TEST' - ) - self.assertEqual( - match.group('params'), - ';ALTREP="http://www.wiz.org"' - ) - - def test_stringToTextValues(self): - """ - Test string lists - """ - self.assertEqual( - stringToTextValues(''), - [''] - ) - self.assertEqual( - stringToTextValues('abcd,efgh'), - ['abcd', 'efgh'] - ) - - def test_stringToPeriod(self): - """ - Test datetime strings - """ - self.assertEqual( - stringToPeriod("19970101T180000Z/19970102T070000Z"), - (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), - datetime.datetime(1997, 1, 2, 7, 0, tzinfo=tzutc())) - ) - self.assertEqual( - stringToPeriod("19970101T180000Z/PT1H"), - (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), - datetime.timedelta(0, 3600)) - ) - - def test_timedeltaToString(self): - """ - Test timedelta strings - """ - self.assertEqual( - timedeltaToString(two_hours), - 'PT2H' - ) - self.assertEqual( - timedeltaToString(datetime.timedelta(minutes=20)), - 'PT20M' - ) - - def test_vtimezone_creation(self): - """ - Test timezones - """ - tzs = dateutil.tz.tzical("test_files/timezones.ics") - pacific = icalendar.TimezoneComponent(tzs.get('US/Pacific')) - self.assertEqual( - str(pacific), - ">" - ) - santiago = icalendar.TimezoneComponent(tzs.get('Santiago')) - self.assertEqual( - str(santiago), - ">" - ) - for year in range(2001, 2010): - for month in (2, 9): - dt = datetime.datetime(year, month, 15, - tzinfo=tzs.get('Santiago')) - self.assertTrue(dt.replace(tzinfo=tzs.get('Santiago')), dt) - - @staticmethod - def test_timezone_serializing(): - """ - Serializing with timezones test - """ - tzs = dateutil.tz.tzical("test_files/timezones.ics") - pacific = tzs.get('US/Pacific') - cal = base.Component('VCALENDAR') - cal.setBehavior(icalendar.VCalendar2_0) - ev = cal.add('vevent') - ev.add('dtstart').value = datetime.datetime(2005, 10, 12, 9, - tzinfo=pacific) - evruleset = rruleset() - evruleset.rrule(rrule(WEEKLY, interval=2, byweekday=[2,4], - until=datetime.datetime(2005, 12, 15, 9))) - evruleset.rrule(rrule(MONTHLY, bymonthday=[-1,-5])) - evruleset.exdate(datetime.datetime(2005, 10, 14, 9, tzinfo=pacific)) - ev.rruleset = evruleset - ev.add('duration').value = datetime.timedelta(hours=1) - - apple = tzs.get('America/Montreal') - ev.dtstart.value = datetime.datetime(2005, 10, 12, 9, tzinfo=apple) - - def test_pytz_timezone_serializing(self): - """ - Serializing with timezones from pytz test - """ - try: - import pytz - except ImportError: - return self.skipTest("pytz not installed") # NOQA - - # Avoid conflicting cached tzinfo from other tests - def unregister_tzid(tzid): - """Clear tzid from icalendar TZID registry""" - if icalendar.getTzid(tzid, False): - icalendar.registerTzid(tzid, None) - - unregister_tzid('US/Eastern') - eastern = pytz.timezone('US/Eastern') - cal = base.Component('VCALENDAR') - cal.setBehavior(icalendar.VCalendar2_0) - ev = cal.add('vevent') - ev.add('dtstart').value = eastern.localize( - datetime.datetime(2008, 10, 12, 9)) - serialized = cal.serialize() - - expected_vtimezone = get_test_file("tz_us_eastern.ics") - self.assertIn( - expected_vtimezone.replace('\r\n', '\n'), - serialized.replace('\r\n', '\n') - ) - - # Exhaustively test all zones (just looking for no errors) - for tzname in pytz.all_timezones: - unregister_tzid(tzname) - tz = icalendar.TimezoneComponent(tzinfo=pytz.timezone(tzname)) - tz.serialize() - - def test_freeBusy(self): - """ - Test freebusy components - """ - test_cal = get_test_file("freebusy.ics") - - vfb = base.newFromBehavior('VFREEBUSY') - vfb.add('uid').value = 'test' - vfb.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) - vfb.add('dtstart').value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc) - vfb.add('dtend').value = vfb.dtstart.value + two_hours - vfb.add('freebusy').value = [(vfb.dtstart.value, two_hours / 2)] - vfb.add('freebusy').value = [(vfb.dtstart.value, vfb.dtend.value)] - - self.assertEqual( - vfb.serialize().replace('\r\n', '\n'), - test_cal.replace('\r\n', '\n') - ) - - def test_availablity(self): - """ - Test availability components - """ - test_cal = get_test_file("availablity.ics") - - vcal = base.newFromBehavior('VAVAILABILITY') - vcal.add('uid').value = 'test' - vcal.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) - vcal.add('dtstart').value = datetime.datetime(2006, 2, 16, 0, tzinfo=utc) - vcal.add('dtend').value = datetime.datetime(2006, 2, 17, 0, tzinfo=utc) - vcal.add('busytype').value = "BUSY" - - av = base.newFromBehavior('AVAILABLE') - av.add('uid').value = 'test1' - av.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) - av.add('dtstart').value = datetime.datetime(2006, 2, 16, 9, tzinfo=utc) - av.add('dtend').value = datetime.datetime(2006, 2, 16, 12, tzinfo=utc) - av.add('summary').value = "Available in the morning" - - vcal.add(av) - - self.assertEqual( - vcal.serialize().replace('\r\n', '\n'), - test_cal.replace('\r\n', '\n') - ) - - def test_recurrence(self): - """ - Ensure date valued UNTILs in rrules are in a reasonable timezone, - and include that day (12/28 in this test) - """ - test_file = get_test_file("recurrence.ics") - cal = base.readOne(test_file) - dates = list(cal.vevent.getrruleset()) - self.assertEqual( - dates[0], - datetime.datetime(2006, 1, 26, 23, 0, tzinfo=tzutc()) - ) - self.assertEqual( - dates[1], - datetime.datetime(2006, 2, 23, 23, 0, tzinfo=tzutc()) - ) - self.assertEqual( - dates[-1], - datetime.datetime(2006, 12, 28, 23, 0, tzinfo=tzutc()) - ) - - def test_recurring_component(self): - """ - Test recurring events - """ - vevent = RecurringComponent(name='VEVENT') - - # init - self.assertTrue(vevent.isNative) - - # rruleset should be None at this point. - # No rules have been passed or created. - self.assertEqual(vevent.rruleset, None) - - # Now add start and rule for recurring event - vevent.add('dtstart').value = datetime.datetime(2005, 1, 19, 9) - vevent.add('rrule').value =u"FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH" - self.assertEqual( - list(vevent.rruleset), - [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)] - ) - self.assertEqual( - list(vevent.getrruleset(addRDate=True)), - [datetime.datetime(2005, 1, 19, 9, 0), datetime.datetime(2005, 1, 20, 9, 0)] - ) - - # Also note that dateutil will expand all-day events (datetime.date values) - # to datetime.datetime value with time 0 and no timezone. - vevent.dtstart.value = datetime.date(2005,3,18) - self.assertEqual( - list(vevent.rruleset), - [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)] - ) - self.assertEqual( - list(vevent.getrruleset(True)), - [datetime.datetime(2005, 3, 18, 0, 0), datetime.datetime(2005, 3, 29, 0, 0)] - ) - - def test_recurrence_without_tz(self): - """ - Test recurring vevent missing any time zone definitions. - """ - test_file = get_test_file("recurrence-without-tz.ics") - cal = base.readOne(test_file) - dates = list(cal.vevent.getrruleset()) - self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0)) - self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0)) - self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0)) - - def test_recurrence_offset_naive(self): - """ - Ensure recurring vevent missing some time zone definitions is - parsing. See isseu #75. - """ - test_file = get_test_file("recurrence-offset-naive.ics") - cal = base.readOne(test_file) - dates = list(cal.vevent.getrruleset()) - self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0)) - self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0)) - self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0)) - - -class TestChangeTZ(unittest.TestCase): - """ - Tests for change_tz.change_tz - """ - class StubCal(object): - class StubEvent(object): - class Node(object): - def __init__(self, value): - self.value = value - - def __init__(self, dtstart, dtend): - self.dtstart = self.Node(dtstart) - self.dtend = self.Node(dtend) - - def __init__(self, dates): - """ - dates is a list of tuples (dtstart, dtend) - """ - self.vevent_list = [self.StubEvent(*d) for d in dates] - - def test_change_tz(self): - """ - Change the timezones of events in a component to a different - timezone - """ - - # Setup - create a stub vevent list - old_tz = dateutil.tz.gettz('UTC') # 0:00 - new_tz = dateutil.tz.gettz('America/Chicago') # -5:00 - - dates = [ - (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=old_tz), - datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=old_tz)), - (datetime.datetime(2010, 12, 31, 23, 59, 59, 0, tzinfo=old_tz), - datetime.datetime(2011, 1, 2, 3, 0, 0, 0, tzinfo=old_tz))] - - cal = self.StubCal(dates) - - # Exercise - change the timezone - change_tz(cal, new_tz, dateutil.tz.gettz('UTC')) - - # Test - that the tzs were converted correctly - expected_new_dates = [ - (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), - datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz)), - (datetime.datetime(2010, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), - datetime.datetime(2011, 1, 1, 21, 0, 0, 0, tzinfo=new_tz))] - - for vevent, expected_datepair in zip(cal.vevent_list, - expected_new_dates): - self.assertEqual(vevent.dtstart.value, expected_datepair[0]) - self.assertEqual(vevent.dtend.value, expected_datepair[1]) - - def test_change_tz_utc_only(self): - """ - Change any UTC timezones of events in a component to a different - timezone - """ - - # Setup - create a stub vevent list - utc_tz = dateutil.tz.gettz('UTC') # 0:00 - non_utc_tz = dateutil.tz.gettz('America/Santiago') # -4:00 - new_tz = dateutil.tz.gettz('America/Chicago') # -5:00 - - dates = [ - (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=utc_tz), - datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=non_utc_tz))] - - cal = self.StubCal(dates) - - # Exercise - change the timezone passing utc_only=True - change_tz(cal, new_tz, dateutil.tz.gettz('UTC'), utc_only=True) - - # Test - that only the utc item has changed - expected_new_dates = [ - (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), - dates[0][1])] - - for vevent, expected_datepair in zip(cal.vevent_list, - expected_new_dates): - self.assertEqual(vevent.dtstart.value, expected_datepair[0]) - self.assertEqual(vevent.dtend.value, expected_datepair[1]) - - def test_change_tz_default(self): - """ - Change the timezones of events in a component to a different - timezone, passing a default timezone that is assumed when the events - don't have one - """ - - # Setup - create a stub vevent list - new_tz = dateutil.tz.gettz('America/Chicago') # -5:00 - - dates = [ - (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=None), - datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=None))] - - cal = self.StubCal(dates) - - # Exercise - change the timezone - change_tz(cal, new_tz, dateutil.tz.gettz('UTC')) - - # Test - that the tzs were converted correctly - expected_new_dates = [ - (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), - datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz))] - - for vevent, expected_datepair in zip(cal.vevent_list, - expected_new_dates): - self.assertEqual(vevent.dtstart.value, expected_datepair[0]) - self.assertEqual(vevent.dtend.value, expected_datepair[1]) - - -if __name__ == '__main__': - unittest.main() +# -*- coding: utf-8 -*- + +from __future__ import print_function + +import datetime +import dateutil +import re +import sys +import unittest +import json + +from dateutil.tz import tzutc +from dateutil.rrule import rrule, rruleset, WEEKLY, MONTHLY + +from vobject import base, iCalendar +from vobject import icalendar, vcard + +from vobject.base import __behaviorRegistry as behavior_registry +from vobject.base import ContentLine, parseLine, ParseError +from vobject.base import readComponents, textLineToContentLine + +from vobject.change_tz import change_tz + +from vobject.icalendar import MultiDateBehavior, PeriodBehavior, \ + RecurringComponent, utc +from vobject.icalendar import parseDtstart, stringToTextValues, \ + stringToPeriod, timedeltaToString + +two_hours = datetime.timedelta(hours=2) + + +def get_test_file(path): + """ + Helper function to open and read test files. + """ + filepath = "test_files/{}".format(path) + if sys.version_info[0] < 3: + # On python 2, this library operates on bytes. + f = open(filepath, 'r') + else: + # On python 3, it operates on unicode. We need to specify an encoding + # for systems for which the preferred encoding isn't utf-8 (e.g windows) + f = open(filepath, 'r', encoding='utf-8') + text = f.read() + f.close() + return text + +class TestCalendarSerializing(unittest.TestCase): + """ + Test creating an iCalendar file + """ + max_diff = None + + def test_scratchbuild(self): + """ + CreateCalendar 2.0 format from scratch + """ + test_cal = get_test_file("simple_2_0_test.ics") + cal = base.newFromBehavior('vcalendar', '2.0') + cal.add('vevent') + cal.vevent.add('dtstart').value = datetime.datetime(2006, 5, 9) + cal.vevent.add('description').value = "Test event" + cal.vevent.add('created').value = \ + datetime.datetime(2006, 1, 1, 10, + tzinfo=dateutil.tz.tzical( + "test_files/timezones.ics").get('US/Pacific')) + cal.vevent.add('uid').value = "Not very random UID" + cal.vevent.add('dtstamp').value = datetime.datetime(2017, 6, 26, 0, tzinfo=tzutc()) + + cal.vevent.add('attendee').value = 'mailto:froelich@example.com' + cal.vevent.attendee.params['CN'] = ['Fröhlich'] + + # Note we're normalizing line endings, because no one got time for that. + self.assertEqual( + cal.serialize().replace('\r\n', '\n'), + test_cal.replace('\r\n', '\n') + ) + + def test_unicode(self): + """ + Test unicode characters + """ + test_cal = get_test_file("utf8_test.ics") + vevent = base.readOne(test_cal).vevent + vevent2 = base.readOne(vevent.serialize()) + self.assertEqual(str(vevent), str(vevent2)) + + self.assertEqual( + vevent.summary.value, + 'The title こんにちはキティ' + ) + + if sys.version_info[0] < 3: + test_cal = test_cal.decode('utf-8') + vevent = base.readOne(test_cal).vevent + vevent2 = base.readOne(vevent.serialize()) + self.assertEqual(str(vevent), str(vevent2)) + self.assertEqual( + vevent.summary.value, + u'The title こんにちはキティ' + ) + + def test_wrapping(self): + """ + Should support input file with a long text field covering multiple lines + """ + test_journal = get_test_file("journal.ics") + vobj = base.readOne(test_journal) + vjournal = base.readOne(vobj.serialize()) + self.assertTrue('Joe, Lisa, and Bob' in vjournal.description.value) + self.assertTrue('Tuesday.\n2.' in vjournal.description.value) + + def test_multiline(self): + """ + Multi-text serialization test + """ + category = base.newFromBehavior('categories') + category.value = ['Random category'] + self.assertEqual( + category.serialize().strip(), + "CATEGORIES:Random category" + ) + + category.value.append('Other category') + self.assertEqual( + category.serialize().strip(), + "CATEGORIES:Random category,Other category" + ) + + def test_semicolon_separated(self): + """ + Semi-colon separated multi-text serialization test + """ + request_status = base.newFromBehavior('request-status') + request_status.value = ['5.1', 'Service unavailable'] + self.assertEqual( + request_status.serialize().strip(), + "REQUEST-STATUS:5.1;Service unavailable" + ) + + @staticmethod + def test_unicode_multiline(): + """ + Test multiline unicode characters + """ + cal = iCalendar() + cal.add('method').value = 'REQUEST' + cal.add('vevent') + cal.vevent.add('created').value = datetime.datetime.now() + cal.vevent.add('summary').value = 'Классное событие' + cal.vevent.add('description').value = ('Классное событие Классное событие Классное событие Классное событие ' + 'Классное событие Классsdssdное событие') + + # json tries to encode as utf-8 and it would break if some chars could not be encoded + json.dumps(cal.serialize()) + + @staticmethod + def test_ical_to_hcal(): + """ + Serializing iCalendar to hCalendar. + + Since Hcalendar is experimental and the behavior doesn't seem to want to load, + This test will have to wait. + + + tzs = dateutil.tz.tzical("test_files/timezones.ics") + cal = base.newFromBehavior('hcalendar') + self.assertEqual( + str(cal.behavior), + "" + ) + cal.add('vevent') + cal.vevent.add('summary').value = "this is a note" + cal.vevent.add('url').value = "http://microformats.org/code/hcalendar/creator" + cal.vevent.add('dtstart').value = datetime.date(2006,2,27) + cal.vevent.add('location').value = "a place" + cal.vevent.add('dtend').value = datetime.date(2006,2,27) + datetime.timedelta(days = 2) + + event2 = cal.add('vevent') + event2.add('summary').value = "Another one" + event2.add('description').value = "The greatest thing ever!" + event2.add('dtstart').value = datetime.datetime(1998, 12, 17, 16, 42, tzinfo = tzs.get('US/Pacific')) + event2.add('location').value = "somewhere else" + event2.add('dtend').value = event2.dtstart.value + datetime.timedelta(days = 6) + hcal = cal.serialize() + """ + #self.assertEqual( + # str(hcal), + # """ + # + # this is a note: + # Monday, February 27 + # - Tuesday, February 28 + # at a place + # + # + # + # Another one: + # Thursday, December 17, 16:42 + # - Wednesday, December 23, 16:42 + # at somewhere else + #
The greatest thing ever!
+ #
+ # """ + #) + + +class TestBehaviors(unittest.TestCase): + """ + Test Behaviors + """ + def test_general_behavior(self): + """ + Tests for behavior registry, getting and creating a behavior. + """ + # Check expected behavior registry. + self.assertEqual( + sorted(behavior_registry.keys()), + ['', 'ACTION', 'ADR', 'AVAILABLE', 'BUSYTYPE', 'CALSCALE', + 'CATEGORIES', 'CLASS', 'COMMENT', 'COMPLETED', 'CONTACT', + 'CREATED', 'DAYLIGHT', 'DESCRIPTION', 'DTEND', 'DTSTAMP', + 'DTSTART', 'DUE', 'DURATION', 'EXDATE', 'EXRULE', 'FN', 'FREEBUSY', + 'LABEL', 'LAST-MODIFIED', 'LOCATION', 'METHOD', 'N', 'ORG', + 'PHOTO', 'PRODID', 'RDATE', 'RECURRENCE-ID', 'RELATED-TO', + 'REQUEST-STATUS', 'RESOURCES', 'RRULE', 'STANDARD', 'STATUS', + 'SUMMARY', 'TRANSP', 'TRIGGER', 'UID', 'VALARM', 'VAVAILABILITY', + 'VCALENDAR', 'VCARD', 'VEVENT', 'VFREEBUSY', 'VJOURNAL', + 'VTIMEZONE', 'VTODO'] + ) + + # test get_behavior + behavior = base.getBehavior('VCALENDAR') + self.assertEqual( + str(behavior), + "" + ) + self.assertTrue(behavior.isComponent) + + self.assertEqual( + base.getBehavior("invalid_name"), + None + ) + # test for ContentLine (not a component) + non_component_behavior = base.getBehavior('RDATE') + self.assertFalse(non_component_behavior.isComponent) + + def test_MultiDateBehavior(self): + """ + Test MultiDateBehavior + """ + parseRDate = MultiDateBehavior.transformToNative + self.assertEqual( + str(parseRDate(textLineToContentLine("RDATE;VALUE=DATE:19970304,19970504,19970704,19970904"))), + "" + ) + self.assertEqual( + str(parseRDate(textLineToContentLine("RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H"))), + "" + ) + + def test_periodBehavior(self): + """ + Test PeriodBehavior + """ + line = ContentLine('test', [], '', isNative=True) + line.behavior = PeriodBehavior + line.value = [(datetime.datetime(2006, 2, 16, 10), two_hours)] + + self.assertEqual( + line.transformFromNative().value, + '20060216T100000/PT2H' + ) + self.assertEqual( + line.transformToNative().value, + [(datetime.datetime(2006, 2, 16, 10, 0), + datetime.timedelta(0, 7200))] + ) + + line.value.append((datetime.datetime(2006, 5, 16, 10), two_hours)) + + self.assertEqual( + line.serialize().strip(), + 'TEST:20060216T100000/PT2H,20060516T100000/PT2H' + ) + + +class TestVTodo(unittest.TestCase): + """ + VTodo Tests + """ + def test_vtodo(self): + """ + Test VTodo + """ + vtodo = get_test_file("vtodo.ics") + obj = base.readOne(vtodo) + obj.vtodo.add('completed') + obj.vtodo.completed.value = datetime.datetime(2015,5,5,13,30) + self.assertEqual(obj.vtodo.completed.serialize()[0:23], + 'COMPLETED:20150505T1330') + obj = base.readOne(obj.serialize()) + self.assertEqual(obj.vtodo.completed.value, + datetime.datetime(2015,5,5,13,30)) + + +class TestVobject(unittest.TestCase): + """ + VObject Tests + """ + max_diff = None + + @classmethod + def setUpClass(cls): + """ + Method for setting up class fixture before running tests in the class. + Fetches test file. + """ + cls.simple_test_cal = get_test_file("simple_test.ics") + + def test_readComponents(self): + """ + Test if reading components correctly + """ + cal = next(readComponents(self.simple_test_cal)) + + self.assertEqual(str(cal), "]>]>") + self.assertEqual(str(cal.vevent.summary), "") + + def test_parseLine(self): + """ + Test line parsing + """ + self.assertEqual(parseLine("BLAH:"), ('BLAH', [], '', None)) + self.assertEqual( + parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904"), + ('RDATE', [], 'VALUE=DATE:19970304,19970504,19970704,19970904', None) + ) + self.assertEqual( + parseLine('DESCRIPTION;ALTREP="http://www.wiz.org":The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA'), + ('DESCRIPTION', [['ALTREP', 'http://www.wiz.org']], 'The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA', None) + ) + self.assertEqual( + parseLine("EMAIL;PREF;INTERNET:john@nowhere.com"), + ('EMAIL', [['PREF'], ['INTERNET']], 'john@nowhere.com', None) + ) + self.assertEqual( + parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com'), + ('EMAIL', [['TYPE', 'blah', 'hah'], ['INTERNET', 'DIGI', 'DERIDOO']], 'john@nowhere.com', None) + ) + self.assertEqual( + parseLine('item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;'), + ('ADR', [['type', 'HOME'], ['type', 'pref']], ';;Reeperbahn 116;Hamburg;;20359;', 'item1') + ) + self.assertRaises(ParseError, parseLine, ":") + + +class TestGeneralFileParsing(unittest.TestCase): + """ + General tests for parsing ics files. + """ + def test_readOne(self): + """ + Test reading first component of ics + """ + cal = get_test_file("silly_test.ics") + silly = base.readOne(cal) + self.assertEqual( + str(silly), + ", , ]>" + ) + self.assertEqual( + str(silly.stuff), + "" + ) + + def test_importing(self): + """ + Test importing ics + """ + cal = get_test_file("standard_test.ics") + c = base.readOne(cal, validate=True) + self.assertEqual( + str(c.vevent.valarm.trigger), + "" + ) + + self.assertEqual( + str(c.vevent.dtstart.value), + "2002-10-28 14:00:00-08:00" + ) + self.assertTrue( + isinstance(c.vevent.dtstart.value, datetime.datetime) + ) + self.assertEqual( + str(c.vevent.dtend.value), + "2002-10-28 15:00:00-08:00" + ) + self.assertTrue( + isinstance(c.vevent.dtend.value, datetime.datetime) + ) + self.assertEqual( + c.vevent.dtstamp.value, + datetime.datetime(2002, 10, 28, 1, 17, 6, tzinfo=tzutc()) + ) + + vevent = c.vevent.transformFromNative() + self.assertEqual( + str(vevent.rrule), + "" + ) + + def test_bad_stream(self): + """ + Test bad ics stream + """ + cal = get_test_file("badstream.ics") + self.assertRaises(ParseError, base.readOne, cal) + + def test_bad_line(self): + """ + Test bad line in ics file + """ + cal = get_test_file("badline.ics") + self.assertRaises(ParseError, base.readOne, cal) + + newcal = base.readOne(cal, ignoreUnreadable=True) + self.assertEqual( + str(newcal.vevent.x_bad_underscore), + '' + ) + + def test_parseParams(self): + """ + Test parsing parameters + """ + self.assertEqual( + base.parseParams(';ALTREP="http://www.wiz.org"'), + [['ALTREP', 'http://www.wiz.org']] + ) + self.assertEqual( + base.parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR'), + [['ALTREP', 'http://www.wiz.org;;', 'Blah', 'Foo'], + ['NEXT', 'Nope'], ['BAR']] + ) + + +class TestVcards(unittest.TestCase): + """ + Test VCards + """ + @classmethod + def setUpClass(cls): + """ + Method for setting up class fixture before running tests in the class. + Fetches test file. + """ + cls.test_file = get_test_file("vcard_with_groups.ics") + cls.card = base.readOne(cls.test_file) + + def test_vcard_creation(self): + """ + Test creating a vCard + """ + vcard = base.newFromBehavior('vcard', '3.0') + self.assertEqual( + str(vcard), + "" + ) + + def test_default_behavior(self): + """ + Default behavior test. + """ + card = self.card + self.assertEqual( + base.getBehavior('note'), + None + ) + self.assertEqual( + str(card.note.value), + "The Mayor of the great city of Goerlitz in the great country of Germany.\nNext line." + ) + + def test_with_groups(self): + """ + vCard groups test + """ + card = self.card + self.assertEqual( + str(card.group), + 'home' + ) + self.assertEqual( + str(card.tel.group), + 'home' + ) + + card.group = card.tel.group = 'new' + self.assertEqual( + str(card.tel.serialize().strip()), + 'new.TEL;TYPE=fax,voice,msg:+49 3581 123456' + ) + self.assertEqual( + str(card.serialize().splitlines()[0]), + 'new.BEGIN:VCARD' + ) + + + def test_vcard_3_parsing(self): + """ + VCARD 3.0 parse test + """ + test_file = get_test_file("simple_3_0_test.ics") + card = base.readOne(test_file) + # value not rendering correctly? + #self.assertEqual( + # card.adr.value, + # "" + #) + self.assertEqual( + card.org.value, + ["University of Novosibirsk", "Department of Octopus Parthenogenesis"] + ) + + for _ in range(3): + new_card = base.readOne(card.serialize()) + self.assertEqual(new_card.org.value, card.org.value) + card = new_card + + +class TestIcalendar(unittest.TestCase): + """ + Tests for icalendar.py + """ + max_diff = None + def test_parseDTStart(self): + """ + Should take a content line and return a datetime object. + """ + self.assertEqual( + parseDtstart(textLineToContentLine("DTSTART:20060509T000000")), + datetime.datetime(2006, 5, 9, 0, 0) + ) + + def test_regexes(self): + """ + Test regex patterns + """ + self.assertEqual( + re.findall(base.patterns['name'], '12foo-bar:yay'), + ['12foo-bar', 'yay'] + ) + self.assertEqual( + re.findall(base.patterns['safe_char'], 'a;b"*,cd'), + ['a', 'b', '*', 'c', 'd'] + ) + self.assertEqual( + re.findall(base.patterns['qsafe_char'], 'a;b"*,cd'), + ['a', ';', 'b', '*', ',', 'c', 'd'] + ) + self.assertEqual( + re.findall(base.patterns['param_value'], + '"quoted";not-quoted;start"after-illegal-quote', + re.VERBOSE), + ['"quoted"', '', 'not-quoted', '', 'start', '', + 'after-illegal-quote', ''] + ) + match = base.line_re.match('TEST;ALTREP="http://www.wiz.org":value:;"') + self.assertEqual( + match.group('value'), + 'value:;"' + ) + self.assertEqual( + match.group('name'), + 'TEST' + ) + self.assertEqual( + match.group('params'), + ';ALTREP="http://www.wiz.org"' + ) + + def test_stringToTextValues(self): + """ + Test string lists + """ + self.assertEqual( + stringToTextValues(''), + [''] + ) + self.assertEqual( + stringToTextValues('abcd,efgh'), + ['abcd', 'efgh'] + ) + + def test_stringToPeriod(self): + """ + Test datetime strings + """ + self.assertEqual( + stringToPeriod("19970101T180000Z/19970102T070000Z"), + (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), + datetime.datetime(1997, 1, 2, 7, 0, tzinfo=tzutc())) + ) + self.assertEqual( + stringToPeriod("19970101T180000Z/PT1H"), + (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), + datetime.timedelta(0, 3600)) + ) + + def test_timedeltaToString(self): + """ + Test timedelta strings + """ + self.assertEqual( + timedeltaToString(two_hours), + 'PT2H' + ) + self.assertEqual( + timedeltaToString(datetime.timedelta(minutes=20)), + 'PT20M' + ) + + def test_vtimezone_creation(self): + """ + Test timezones + """ + tzs = dateutil.tz.tzical("test_files/timezones.ics") + pacific = icalendar.TimezoneComponent(tzs.get('US/Pacific')) + self.assertEqual( + str(pacific), + ">" + ) + santiago = icalendar.TimezoneComponent(tzs.get('Santiago')) + self.assertEqual( + str(santiago), + ">" + ) + for year in range(2001, 2010): + for month in (2, 9): + dt = datetime.datetime(year, month, 15, + tzinfo=tzs.get('Santiago')) + self.assertTrue(dt.replace(tzinfo=tzs.get('Santiago')), dt) + + @staticmethod + def test_timezone_serializing(): + """ + Serializing with timezones test + """ + tzs = dateutil.tz.tzical("test_files/timezones.ics") + pacific = tzs.get('US/Pacific') + cal = base.Component('VCALENDAR') + cal.setBehavior(icalendar.VCalendar2_0) + ev = cal.add('vevent') + ev.add('dtstart').value = datetime.datetime(2005, 10, 12, 9, + tzinfo=pacific) + evruleset = rruleset() + evruleset.rrule(rrule(WEEKLY, interval=2, byweekday=[2,4], + until=datetime.datetime(2005, 12, 15, 9))) + evruleset.rrule(rrule(MONTHLY, bymonthday=[-1,-5])) + evruleset.exdate(datetime.datetime(2005, 10, 14, 9, tzinfo=pacific)) + ev.rruleset = evruleset + ev.add('duration').value = datetime.timedelta(hours=1) + + apple = tzs.get('America/Montreal') + ev.dtstart.value = datetime.datetime(2005, 10, 12, 9, tzinfo=apple) + + def test_pytz_timezone_serializing(self): + """ + Serializing with timezones from pytz test + """ + try: + import pytz + except ImportError: + return self.skipTest("pytz not installed") # NOQA + + # Avoid conflicting cached tzinfo from other tests + def unregister_tzid(tzid): + """Clear tzid from icalendar TZID registry""" + if icalendar.getTzid(tzid, False): + icalendar.registerTzid(tzid, None) + + unregister_tzid('US/Eastern') + eastern = pytz.timezone('US/Eastern') + cal = base.Component('VCALENDAR') + cal.setBehavior(icalendar.VCalendar2_0) + ev = cal.add('vevent') + ev.add('dtstart').value = eastern.localize( + datetime.datetime(2008, 10, 12, 9)) + serialized = cal.serialize() + + expected_vtimezone = get_test_file("tz_us_eastern.ics") + self.assertIn( + expected_vtimezone.replace('\r\n', '\n'), + serialized.replace('\r\n', '\n') + ) + + # Exhaustively test all zones (just looking for no errors) + for tzname in pytz.all_timezones: + unregister_tzid(tzname) + tz = icalendar.TimezoneComponent(tzinfo=pytz.timezone(tzname)) + tz.serialize() + + def test_freeBusy(self): + """ + Test freebusy components + """ + test_cal = get_test_file("freebusy.ics") + + vfb = base.newFromBehavior('VFREEBUSY') + vfb.add('uid').value = 'test' + vfb.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) + vfb.add('dtstart').value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc) + vfb.add('dtend').value = vfb.dtstart.value + two_hours + vfb.add('freebusy').value = [(vfb.dtstart.value, two_hours / 2)] + vfb.add('freebusy').value = [(vfb.dtstart.value, vfb.dtend.value)] + + self.assertEqual( + vfb.serialize().replace('\r\n', '\n'), + test_cal.replace('\r\n', '\n') + ) + + def test_availablity(self): + """ + Test availability components + """ + test_cal = get_test_file("availablity.ics") + + vcal = base.newFromBehavior('VAVAILABILITY') + vcal.add('uid').value = 'test' + vcal.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) + vcal.add('dtstart').value = datetime.datetime(2006, 2, 16, 0, tzinfo=utc) + vcal.add('dtend').value = datetime.datetime(2006, 2, 17, 0, tzinfo=utc) + vcal.add('busytype').value = "BUSY" + + av = base.newFromBehavior('AVAILABLE') + av.add('uid').value = 'test1' + av.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) + av.add('dtstart').value = datetime.datetime(2006, 2, 16, 9, tzinfo=utc) + av.add('dtend').value = datetime.datetime(2006, 2, 16, 12, tzinfo=utc) + av.add('summary').value = "Available in the morning" + + vcal.add(av) + + self.assertEqual( + vcal.serialize().replace('\r\n', '\n'), + test_cal.replace('\r\n', '\n') + ) + + def test_recurrence(self): + """ + Ensure date valued UNTILs in rrules are in a reasonable timezone, + and include that day (12/28 in this test) + """ + test_file = get_test_file("recurrence.ics") + cal = base.readOne(test_file) + dates = list(cal.vevent.getrruleset()) + self.assertEqual( + dates[0], + datetime.datetime(2006, 1, 26, 23, 0, tzinfo=tzutc()) + ) + self.assertEqual( + dates[1], + datetime.datetime(2006, 2, 23, 23, 0, tzinfo=tzutc()) + ) + self.assertEqual( + dates[-1], + datetime.datetime(2006, 12, 28, 23, 0, tzinfo=tzutc()) + ) + + def test_recurring_component(self): + """ + Test recurring events + """ + vevent = RecurringComponent(name='VEVENT') + + # init + self.assertTrue(vevent.isNative) + + # rruleset should be None at this point. + # No rules have been passed or created. + self.assertEqual(vevent.rruleset, None) + + # Now add start and rule for recurring event + vevent.add('dtstart').value = datetime.datetime(2005, 1, 19, 9) + vevent.add('rrule').value =u"FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH" + self.assertEqual( + list(vevent.rruleset), + [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)] + ) + self.assertEqual( + list(vevent.getrruleset(addRDate=True)), + [datetime.datetime(2005, 1, 19, 9, 0), datetime.datetime(2005, 1, 20, 9, 0)] + ) + + # Also note that dateutil will expand all-day events (datetime.date values) + # to datetime.datetime value with time 0 and no timezone. + vevent.dtstart.value = datetime.date(2005,3,18) + self.assertEqual( + list(vevent.rruleset), + [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)] + ) + self.assertEqual( + list(vevent.getrruleset(True)), + [datetime.datetime(2005, 3, 18, 0, 0), datetime.datetime(2005, 3, 29, 0, 0)] + ) + + def test_recurrence_without_tz(self): + """ + Test recurring vevent missing any time zone definitions. + """ + test_file = get_test_file("recurrence-without-tz.ics") + cal = base.readOne(test_file) + dates = list(cal.vevent.getrruleset()) + self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0)) + self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0)) + self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0)) + + def test_recurrence_offset_naive(self): + """ + Ensure recurring vevent missing some time zone definitions is + parsing. See isseu #75. + """ + test_file = get_test_file("recurrence-offset-naive.ics") + cal = base.readOne(test_file) + dates = list(cal.vevent.getrruleset()) + self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0)) + self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0)) + self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0)) + + +class TestChangeTZ(unittest.TestCase): + """ + Tests for change_tz.change_tz + """ + class StubCal(object): + class StubEvent(object): + class Node(object): + def __init__(self, value): + self.value = value + + def __init__(self, dtstart, dtend): + self.dtstart = self.Node(dtstart) + self.dtend = self.Node(dtend) + + def __init__(self, dates): + """ + dates is a list of tuples (dtstart, dtend) + """ + self.vevent_list = [self.StubEvent(*d) for d in dates] + + def test_change_tz(self): + """ + Change the timezones of events in a component to a different + timezone + """ + + # Setup - create a stub vevent list + old_tz = dateutil.tz.gettz('UTC') # 0:00 + new_tz = dateutil.tz.gettz('America/Chicago') # -5:00 + + dates = [ + (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=old_tz), + datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=old_tz)), + (datetime.datetime(2010, 12, 31, 23, 59, 59, 0, tzinfo=old_tz), + datetime.datetime(2011, 1, 2, 3, 0, 0, 0, tzinfo=old_tz))] + + cal = self.StubCal(dates) + + # Exercise - change the timezone + change_tz(cal, new_tz, dateutil.tz.gettz('UTC')) + + # Test - that the tzs were converted correctly + expected_new_dates = [ + (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), + datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz)), + (datetime.datetime(2010, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), + datetime.datetime(2011, 1, 1, 21, 0, 0, 0, tzinfo=new_tz))] + + for vevent, expected_datepair in zip(cal.vevent_list, + expected_new_dates): + self.assertEqual(vevent.dtstart.value, expected_datepair[0]) + self.assertEqual(vevent.dtend.value, expected_datepair[1]) + + def test_change_tz_utc_only(self): + """ + Change any UTC timezones of events in a component to a different + timezone + """ + + # Setup - create a stub vevent list + utc_tz = dateutil.tz.gettz('UTC') # 0:00 + non_utc_tz = dateutil.tz.gettz('America/Santiago') # -4:00 + new_tz = dateutil.tz.gettz('America/Chicago') # -5:00 + + dates = [ + (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=utc_tz), + datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=non_utc_tz))] + + cal = self.StubCal(dates) + + # Exercise - change the timezone passing utc_only=True + change_tz(cal, new_tz, dateutil.tz.gettz('UTC'), utc_only=True) + + # Test - that only the utc item has changed + expected_new_dates = [ + (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), + dates[0][1])] + + for vevent, expected_datepair in zip(cal.vevent_list, + expected_new_dates): + self.assertEqual(vevent.dtstart.value, expected_datepair[0]) + self.assertEqual(vevent.dtend.value, expected_datepair[1]) + + def test_change_tz_default(self): + """ + Change the timezones of events in a component to a different + timezone, passing a default timezone that is assumed when the events + don't have one + """ + + # Setup - create a stub vevent list + new_tz = dateutil.tz.gettz('America/Chicago') # -5:00 + + dates = [ + (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=None), + datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=None))] + + cal = self.StubCal(dates) + + # Exercise - change the timezone + change_tz(cal, new_tz, dateutil.tz.gettz('UTC')) + + # Test - that the tzs were converted correctly + expected_new_dates = [ + (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), + datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz))] + + for vevent, expected_datepair in zip(cal.vevent_list, + expected_new_dates): + self.assertEqual(vevent.dtstart.value, expected_datepair[0]) + self.assertEqual(vevent.dtend.value, expected_datepair[1]) + + +class TestJcard(unittest.TestCase): + def test_parse(self): + jcardsString = get_test_file("jcard.json") + card = vcard.fromJCards(jcardsString) + + self.assertEqual(card.org.value, ["Viagenie"]) + + for _ in range(3): + new_card = base.readOne(card.serialize()) + self.assertEqual(new_card.org.value, card.org.value) + card = new_card + + +if __name__ == '__main__': + unittest.main()