# -*- coding: utf-8 -*- from __future__ import unicode_literals import icalendar import os import textwrap import unittest class IcalendarTestCase (unittest.TestCase): def test_long_lines(self): from ..parser import Contentlines, Contentline c = Contentlines([Contentline('BEGIN:VEVENT')]) c.append(Contentline(''.join('123456789 ' * 10))) self.assertEqual( c.to_ical(), b'BEGIN:VEVENT\r\n123456789 123456789 123456789 123456789 ' b'123456789 123456789 123456789 1234\r\n 56789 123456789 ' b'123456789 \r\n' ) # from doctests # Notice that there is an extra empty string in the end of the content # lines. That is so they can be easily joined with: # '\r\n'.join(contentlines)) self.assertEqual(Contentlines.from_ical('A short line\r\n'), ['A short line', '']) self.assertEqual(Contentlines.from_ical('A faked\r\n long line\r\n'), ['A faked long line', '']) self.assertEqual( Contentlines.from_ical('A faked\r\n long line\r\nAnd another ' 'lin\r\n\te that is folded\r\n'), ['A faked long line', 'And another line that is folded', ''] ) def test_contentline_class(self): from ..parser import Contentline, Parameters from ..prop import vText self.assertEqual( Contentline('Si meliora dies, ut vina, poemata reddit').to_ical(), b'Si meliora dies, ut vina, poemata reddit' ) # A long line gets folded c = Contentline(''.join(['123456789 '] * 10)).to_ical() self.assertEqual( c, (b'123456789 123456789 123456789 123456789 123456789 123456789 ' b'123456789 1234\r\n 56789 123456789 123456789 ') ) # A folded line gets unfolded self.assertEqual( Contentline.from_ical(c), ('123456789 123456789 123456789 123456789 123456789 123456789 ' '123456789 123456789 123456789 123456789 ') ) # http://tools.ietf.org/html/rfc5545#section-3.3.11 # An intentional formatted text line break MUST only be included in # a "TEXT" property value by representing the line break with the # character sequence of BACKSLASH, followed by a LATIN SMALL LETTER # N or a LATIN CAPITAL LETTER N, that is "\n" or "\N". # Newlines are not allwoed in content lines self.assertRaises(AssertionError, Contentline, b'1234\r\n\r\n1234') self.assertEqual( Contentline('1234\\n\\n1234').to_ical(), b'1234\\n\\n1234' ) # We do not fold within a UTF-8 character c = Contentline(b'This line has a UTF-8 character where it should be ' b'folded. Make sure it g\xc3\xabts folded before that ' b'character.') self.assertIn(b'\xc3\xab', c.to_ical()) # Another test of the above c = Contentline(b'x' * 73 + b'\xc3\xab' + b'\\n ' + b'y' * 10) self.assertEqual(c.to_ical().count(b'\xc3'), 1) # Don't fail if we fold a line that is exactly X times 74 characters # long c = Contentline(''.join(['x'] * 148)).to_ical() # It can parse itself into parts, # which is a tuple of (name, params, vals) self.assertEqual( Contentline('dtstart:20050101T120000').parts(), ('dtstart', Parameters({}), '20050101T120000') ) self.assertEqual( Contentline('dtstart;value=datetime:20050101T120000').parts(), ('dtstart', Parameters({'VALUE': 'datetime'}), '20050101T120000') ) c = Contentline('ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:' 'MAILTO:maxm@example.com') self.assertEqual( c.parts(), ('ATTENDEE', Parameters({'ROLE': 'REQ-PARTICIPANT', 'CN': 'Max Rasmussen'}), 'MAILTO:maxm@example.com') ) self.assertEqual( c.to_ical().decode('utf-8'), 'ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:' 'MAILTO:maxm@example.com' ) # and back again # NOTE: we are quoting property values with spaces in it. parts = ('ATTENDEE', Parameters({'ROLE': 'REQ-PARTICIPANT', 'CN': 'Max Rasmussen'}), 'MAILTO:maxm@example.com') self.assertEqual( Contentline.from_parts(*parts), 'ATTENDEE;CN="Max Rasmussen";ROLE=REQ-PARTICIPANT:' 'MAILTO:maxm@example.com' ) # and again parts = ('ATTENDEE', Parameters(), 'MAILTO:maxm@example.com') self.assertEqual( Contentline.from_parts(*parts), 'ATTENDEE:MAILTO:maxm@example.com' ) # A value can also be any of the types defined in PropertyValues parts = ('ATTENDEE', Parameters(), vText('MAILTO:test@example.com')) self.assertEqual( Contentline.from_parts(*parts), 'ATTENDEE:MAILTO:test@example.com' ) # A value in UTF-8 parts = ('SUMMARY', Parameters(), vText('INternational char æ ø å')) self.assertEqual( Contentline.from_parts(*parts), 'SUMMARY:INternational char æ ø å' ) # A value can also be unicode parts = ('SUMMARY', Parameters(), vText('INternational char æ ø å')) self.assertEqual( Contentline.from_parts(*parts), 'SUMMARY:INternational char æ ø å' ) # Traversing could look like this. name, params, vals = c.parts() self.assertEqual(name, 'ATTENDEE') self.assertEqual(vals, 'MAILTO:maxm@example.com') self.assertEqual( sorted(params.items()), sorted([('ROLE', 'REQ-PARTICIPANT'), ('CN', 'Max Rasmussen')]) ) # And the traditional failure with self.assertRaisesRegexp( ValueError, 'Content line could not be parsed into parts' ): Contentline('ATTENDEE;maxm@example.com').parts() # Another failure: with self.assertRaisesRegexp( ValueError, 'Content line could not be parsed into parts' ): Contentline(':maxm@example.com').parts() self.assertEqual( Contentline('key;param=:value').parts(), ('key', Parameters({'PARAM': ''}), 'value') ) self.assertEqual( Contentline('key;param="pvalue":value').parts(), ('key', Parameters({'PARAM': 'pvalue'}), 'value') ) # Should bomb on missing param: with self.assertRaisesRegexp( ValueError, 'Content line could not be parsed into parts' ): Contentline.from_ical("k;:no param").parts() self.assertEqual( Contentline('key;param=pvalue:value', strict=False).parts(), ('key', Parameters({'PARAM': 'pvalue'}), 'value') ) # If strict is set to True, uppercase param values that are not # double-quoted, this is because the spec says non-quoted params are # case-insensitive. self.assertEqual( Contentline('key;param=pvalue:value', strict=True).parts(), ('key', Parameters({'PARAM': 'PVALUE'}), 'value') ) self.assertEqual( Contentline('key;param="pValue":value', strict=True).parts(), ('key', Parameters({'PARAM': 'pValue'}), 'value') ) contains_base64 = ( 'X-APPLE-STRUCTURED-LOCATION;' 'VALUE=URI;X-ADDRESS="Kaiserliche Hofburg, 1010 Wien";' 'X-APPLE-MAPKIT-HANDLE=CAESxQEZgr3QZXJyZWljaA==;' 'X-APPLE-RADIUS=328.7978217977285;X-APPLE-REFERENCEFRAME=1;' 'X-TITLE=Heldenplatz:geo:48.206686,16.363235' ).encode('utf-8') self.assertEqual( Contentline(contains_base64, strict=True).parts(), ('X-APPLE-STRUCTURED-LOCATION', Parameters({ 'X-APPLE-RADIUS': '328.7978217977285', 'X-ADDRESS': 'Kaiserliche Hofburg, 1010 Wien', 'X-APPLE-REFERENCEFRAME': '1', 'X-TITLE': 'HELDENPLATZ', 'X-APPLE-MAPKIT-HANDLE': 'CAESXQEZGR3QZXJYZWLJAA==', 'VALUE': 'URI', }), 'geo:48.206686,16.363235' ) ) def test_fold_line(self): from ..parser import foldline self.assertEqual(foldline('foo'), 'foo') self.assertEqual( foldline("Lorem ipsum dolor sit amet, consectetur adipiscing " "elit. Vestibulum convallis imperdiet dui posuere."), ('Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' 'Vestibulum conval\r\n lis imperdiet dui posuere.') ) # I don't really get this test # at least just but bytes in there # porting it to "run" under python 2 & 3 makes it not much better with self.assertRaises(AssertionError): foldline('привет'.encode('utf-8'), limit=3) self.assertEqual(foldline('foobar', limit=4), 'foo\r\n bar') self.assertEqual( foldline('Lorem ipsum dolor sit amet, consectetur adipiscing elit' '. Vestibulum convallis imperdiet dui posuere.'), ('Lorem ipsum dolor sit amet, consectetur adipiscing elit.' ' Vestibulum conval\r\n lis imperdiet dui posuere.') ) self.assertEqual( foldline('DESCRIPTION:АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯ'), 'DESCRIPTION:АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭ\r\n ЮЯ' ) def test_value_double_quoting(self): from ..parser import dquote self.assertEqual(dquote('Max'), 'Max') self.assertEqual(dquote('Rasmussen, Max'), '"Rasmussen, Max"') self.assertEqual(dquote('name:value'), '"name:value"') def test_q_split(self): from ..parser import q_split self.assertEqual(q_split('Max,Moller,"Rasmussen, Max"'), ['Max', 'Moller', '"Rasmussen, Max"']) def test_q_split_bin(self): from ..parser import q_split for s in ('X-SOMETHING=ABCDE==', ',,,'): for maxsplit in range(-1, 3): self.assertEqual(q_split(s, '=', maxsplit=maxsplit), s.split('=', maxsplit)) def test_q_join(self): from ..parser import q_join self.assertEqual(q_join(['Max', 'Moller', 'Rasmussen, Max']), 'Max,Moller,"Rasmussen, Max"') class TestEncoding(unittest.TestCase): def test_broken_property(self): """ Test if error messages are encode properly. """ broken_ical = textwrap.dedent(""" BEGIN:VCALENDAR BEGIN:VEVENT SUMMARY:An Event with too many semicolons DTSTART;;VALUE=DATE-TIME:20140409T093000 UID:abc END:VEVENT END:VCALENDAR """) cal = icalendar.Calendar.from_ical(broken_ical) for event in cal.walk('vevent'): self.assertEqual(len(event.errors), 1, 'Not the right amount of errors.') error = event.errors[0][1] self.assertTrue(error.startswith('Content line could not be parsed into parts')) def test_apple_xlocation(self): """ Test if we support base64 encoded binary data in parameter values. """ directory = os.path.dirname(__file__) with open(os.path.join(directory, 'x_location.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) for event in cal.walk('vevent'): self.assertEqual(len(event.errors), 0, 'Got too many errors')