Varia's website
https://varia.zone
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
324 lines
12 KiB
324 lines
12 KiB
2 weeks ago
|
# -*- 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')
|