Jellyfin(8096), OrbStack(8097) 포트 충돌으로 변경. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
490 lines
16 KiB
Python
490 lines
16 KiB
Python
import itertools
|
|
import re
|
|
from datetime import datetime, timedelta
|
|
|
|
import pytest
|
|
|
|
import icalendar
|
|
from icalendar import prop
|
|
from icalendar.cal import Calendar, Component, Event
|
|
from icalendar.prop import tzid_from_dt
|
|
|
|
|
|
def test_cal_Component(calendar_component):
|
|
"""A component is like a dictionary with extra methods and attributes."""
|
|
assert calendar_component
|
|
assert calendar_component.is_empty()
|
|
|
|
|
|
def test_nonempty_calendar_component(calendar_component):
|
|
"""Every key defines a property.A property can consist of either a
|
|
single item. This can be set with a single value...
|
|
"""
|
|
calendar_component["prodid"] = "-//max m//icalendar.mxm.dk/"
|
|
assert not calendar_component.is_empty()
|
|
assert calendar_component == Calendar({"PRODID": "-//max m//icalendar.mxm.dk/"})
|
|
|
|
# or with a list
|
|
calendar_component["ATTENDEE"] = ["Max M", "Rasmussen"]
|
|
assert calendar_component == Calendar(
|
|
{"ATTENDEE": ["Max M", "Rasmussen"], "PRODID": "-//max m//icalendar.mxm.dk/"}
|
|
)
|
|
|
|
|
|
def test_add_multiple_values(event_component):
|
|
"""add multiple values to a property.
|
|
|
|
If you use the add method you don't have to considder if a value is
|
|
a list or not.
|
|
"""
|
|
# add multiple values at once
|
|
event_component.add("attendee", ["test@test.com", "test2@test.com"])
|
|
|
|
# or add one per line
|
|
event_component.add("attendee", "maxm@mxm.dk")
|
|
event_component.add("attendee", "test@example.dk")
|
|
|
|
# add again multiple values at once to very concatenaton of lists
|
|
event_component.add("attendee", ["test3@test.com", "test4@test.com"])
|
|
|
|
assert event_component == Event(
|
|
{
|
|
"ATTENDEE": [
|
|
prop.vCalAddress("test@test.com"),
|
|
prop.vCalAddress("test2@test.com"),
|
|
prop.vCalAddress("maxm@mxm.dk"),
|
|
prop.vCalAddress("test@example.dk"),
|
|
prop.vCalAddress("test3@test.com"),
|
|
prop.vCalAddress("test4@test.com"),
|
|
]
|
|
}
|
|
)
|
|
|
|
|
|
def test_get_content_directly(c):
|
|
"""You can get the values back directly ..."""
|
|
c.add("prodid", "-//my product//")
|
|
assert c["prodid"] == prop.vText("-//my product//")
|
|
# ... or decoded to a python type
|
|
assert c.decoded("prodid") == b"-//my product//"
|
|
|
|
|
|
def test_get_default_value(c):
|
|
"""With default values for non existing properties"""
|
|
assert c.decoded("version", "No Version") == "No Version"
|
|
|
|
|
|
def test_default_list_example(c):
|
|
c.add("rdate", [datetime(2013, 3, 28), datetime(2013, 3, 27)])
|
|
assert isinstance(c.decoded("rdate"), prop.vDDDLists)
|
|
|
|
|
|
def test_render_component(calendar_component):
|
|
"""The component can render itself in the RFC 5545 format."""
|
|
calendar_component.add("attendee", "Max M")
|
|
assert (
|
|
calendar_component.to_ical()
|
|
== b"BEGIN:VCALENDAR\r\nATTENDEE:Max M\r\nEND:VCALENDAR\r\n"
|
|
)
|
|
|
|
|
|
def test_nested_component_event_ics(filled_event_component):
|
|
"""Check the ical string of the event component."""
|
|
assert filled_event_component.to_ical() == (
|
|
b"BEGIN:VEVENT\r\nDTEND:20000102T000000\r\n"
|
|
+ b"DTSTART:20000101T000000\r\nSUMMARY:A brief history of time\r"
|
|
+ b"\nEND:VEVENT\r\n"
|
|
)
|
|
|
|
|
|
def test_nested_components(calendar_component, filled_event_component):
|
|
"""Components can be nested, so You can add a subcomponent. Eg a calendar
|
|
holds events."""
|
|
self.assertEqual(
|
|
calendar_component.subcomponents,
|
|
[
|
|
Event(
|
|
{
|
|
"DTEND": "20000102T000000",
|
|
"DTSTART": "20000101T000000",
|
|
"SUMMARY": "A brief history of time",
|
|
}
|
|
)
|
|
],
|
|
)
|
|
|
|
|
|
def test_walk_filled_calendar_component(calendar_component, filled_event_component):
|
|
"""We can walk over nested componentes with the walk method."""
|
|
assert [i.name for i in calendar_component.walk()] == ["VCALENDAR", "VEVENT"]
|
|
|
|
|
|
def test_filter_walk(calendar_component, filled_event_component):
|
|
"""We can also just walk over specific component types, by filtering
|
|
them on their name."""
|
|
assert [i.name for i in calendar_component.walk("VEVENT")] == ["VEVENT"]
|
|
assert [i["dtstart"] for i in calendar_component.walk("VEVENT")] == [
|
|
"20000101T000000"
|
|
]
|
|
|
|
|
|
def test_recursive_property_items(calendar_component, filled_event_component):
|
|
"""We can enumerate property items recursively with the property_items
|
|
method."""
|
|
calendar_component.add("attendee", "Max M")
|
|
assert calendar_component.property_items() == [
|
|
("BEGIN", b"VCALENDAR"),
|
|
("ATTENDEE", prop.vCalAddress("Max M")),
|
|
("BEGIN", b"VEVENT"),
|
|
("DTEND", "20000102T000000"),
|
|
("DTSTART", "20000101T000000"),
|
|
("SUMMARY", "A brief history of time"),
|
|
("END", b"VEVENT"),
|
|
("END", b"VCALENDAR"),
|
|
]
|
|
|
|
|
|
def test_flat_property_items(calendar_component, filled_event_component):
|
|
"""We can also enumerate property items just under the component."""
|
|
assert calendar_component.property_items(recursive=False) == [
|
|
("BEGIN", b"VCALENDAR"),
|
|
("ATTENDEE", prop.vCalAddress("Max M")),
|
|
("END", b"VCALENDAR"),
|
|
]
|
|
|
|
|
|
def test_flat_property_items(filled_event_component):
|
|
"""Flat enumeration on the event."""
|
|
assert filled_event_component.property_items(recursive=False) == [
|
|
("BEGIN", b"VEVENT"),
|
|
("DTEND", "20000102T000000"),
|
|
("DTSTART", "20000101T000000"),
|
|
("SUMMARY", "A brief history of time"),
|
|
("END", b"VEVENT"),
|
|
]
|
|
|
|
|
|
def test_indent():
|
|
"""Text fields which span multiple mulitple lines require proper indenting"""
|
|
c = Calendar()
|
|
c["description"] = "Paragraph one\n\nParagraph two"
|
|
assert c.to_ical() == (
|
|
b"BEGIN:VCALENDAR\r\nDESCRIPTION:Paragraph one\\n\\nParagraph two"
|
|
+ b"\r\nEND:VCALENDAR\r\n"
|
|
)
|
|
|
|
|
|
def test_INLINE_properties(calendar_with_resources):
|
|
"""INLINE properties have their values on one property line. Note the
|
|
double quoting of the value with a colon in it.
|
|
"""
|
|
assert calendar_with_resources == Calendar(
|
|
{"RESOURCES": 'Chair, Table, "Room: 42"'}
|
|
)
|
|
assert calendar_with_resources.to_ical() == (
|
|
b'BEGIN:VCALENDAR\r\nRESOURCES:Chair\\, Table\\, "Room: 42"\r\n'
|
|
+ b"END:VCALENDAR\r\n"
|
|
)
|
|
|
|
|
|
def test_get_inline(calendar_with_resources):
|
|
"""The inline values must be handled by the get_inline() and
|
|
set_inline() methods.
|
|
"""
|
|
assert calendar_with_resources.get_inline("resources", decode=0) == [
|
|
"Chair",
|
|
"Table",
|
|
"Room: 42",
|
|
]
|
|
|
|
|
|
def test_get_inline_decoded(calendar_with_resources):
|
|
"""These can also be decoded"""
|
|
assert calendar_with_resources.get_inline("resources", decode=1) == [
|
|
b"Chair",
|
|
b"Table",
|
|
b"Room: 42",
|
|
]
|
|
|
|
|
|
def test_set_inline(calendar_with_resources):
|
|
"""You can set them directly ..."""
|
|
calendar_with_resources.set_inline(
|
|
"resources", ["A", "List", "of", "some, recources"], encode=1
|
|
)
|
|
assert calendar_with_resources["resources"] == 'A,List,of,"some, recources"'
|
|
assert calendar_with_resources.get_inline("resources", decode=0) == [
|
|
"A",
|
|
"List",
|
|
"of",
|
|
"some, recources",
|
|
]
|
|
|
|
|
|
def test_inline_free_busy_inline(c):
|
|
c["freebusy"] = (
|
|
"19970308T160000Z/PT3H,19970308T200000Z/PT1H,"
|
|
+ "19970308T230000Z/19970309T000000Z"
|
|
)
|
|
assert c.get_inline("freebusy", decode=0) == [
|
|
"19970308T160000Z/PT3H",
|
|
"19970308T200000Z/PT1H",
|
|
"19970308T230000Z/19970309T000000Z",
|
|
]
|
|
|
|
freebusy = c.get_inline("freebusy", decode=1)
|
|
assert isinstance(freebusy[0][0], datetime)
|
|
assert isinstance(freebusy[0][1], timedelta)
|
|
|
|
|
|
def test_cal_Component_add(comp, tzp):
|
|
"""Test the for timezone correctness: dtstart should preserve it's
|
|
timezone, created, dtstamp and last-modified must be in UTC.
|
|
"""
|
|
comp.add("dtstart", tzp.localize(datetime(2010, 10, 10, 10, 0, 0), "Europe/Vienna"))
|
|
comp.add("created", datetime(2010, 10, 10, 12, 0, 0))
|
|
comp.add("dtstamp", tzp.localize(datetime(2010, 10, 10, 14, 0, 0), "Europe/Vienna"))
|
|
comp.add("last-modified", tzp.localize_utc(datetime(2010, 10, 10, 16, 0, 0)))
|
|
|
|
lines = comp.to_ical().splitlines()
|
|
assert b"DTSTART;TZID=Europe/Vienna:20101010T100000" in lines
|
|
assert b"CREATED:20101010T120000Z" in lines
|
|
assert b"DTSTAMP:20101010T120000Z" in lines
|
|
assert b"LAST-MODIFIED:20101010T160000Z" in lines
|
|
|
|
|
|
def test_cal_Component_add_no_reencode(comp):
|
|
"""Already encoded values should not be re-encoded."""
|
|
comp.add("ATTACH", "me")
|
|
comp.add("ATTACH", "you", encode=False)
|
|
binary = prop.vBinary("us")
|
|
comp.add("ATTACH", binary)
|
|
|
|
assert comp["ATTACH"] == ["me", "you", binary]
|
|
|
|
|
|
def test_cal_Component_add_property_parameter(comp):
|
|
"""Test the for timezone correctness: dtstart should preserve it's
|
|
timezone, crated, dtstamp and last-modified must be in UTC.
|
|
"""
|
|
comp.add("X-TEST-PROP", "tryout.", parameters={"prop1": "val1", "prop2": "val2"})
|
|
lines = comp.to_ical().splitlines()
|
|
assert b"X-TEST-PROP;PROP1=val1;PROP2=val2:tryout." in lines
|
|
|
|
|
|
comp_prop = pytest.mark.parametrize(
|
|
"component_name, property_name",
|
|
[
|
|
("VEVENT", "DTSTART"),
|
|
("VEVENT", "DTEND"),
|
|
("VEVENT", "RECURRENCE-ID"),
|
|
("VTODO", "DUE"),
|
|
],
|
|
)
|
|
|
|
|
|
@comp_prop
|
|
def test_cal_Component_from_ical(component_name, property_name, tzp):
|
|
"""Check for proper handling of TZID parameter of datetime properties"""
|
|
component_str = "BEGIN:" + component_name + "\n"
|
|
component_str += property_name + ";TZID=America/Denver:"
|
|
component_str += "20120404T073000\nEND:" + component_name
|
|
component = Component.from_ical(component_str)
|
|
assert tzid_from_dt(component[property_name].dt) == "America/Denver"
|
|
|
|
|
|
@comp_prop
|
|
def test_cal_Component_from_ical_2(component_name, property_name, tzp):
|
|
"""Check for proper handling of TZID parameter of datetime properties"""
|
|
component_str = "BEGIN:" + component_name + "\n"
|
|
component_str += property_name + ":"
|
|
component_str += "20120404T073000\nEND:" + component_name
|
|
component = Component.from_ical(component_str)
|
|
assert component[property_name].dt.tzinfo == None
|
|
|
|
|
|
def test_cal_Component_to_ical_property_order():
|
|
component_str = [
|
|
b"BEGIN:VEVENT",
|
|
b"DTSTART:19970714T170000Z",
|
|
b"DTEND:19970715T035959Z",
|
|
b"SUMMARY:Bastille Day Party",
|
|
b"END:VEVENT",
|
|
]
|
|
component = Component.from_ical(b"\r\n".join(component_str))
|
|
|
|
sorted_str = component.to_ical().splitlines()
|
|
assert sorted_str != component_str
|
|
assert set(sorted_str) == set(component_str)
|
|
|
|
preserved_str = component.to_ical(sorted=False).splitlines()
|
|
assert preserved_str == component_str
|
|
|
|
|
|
def test_cal_Component_to_ical_parameter_order():
|
|
component_str = [
|
|
b"BEGIN:VEVENT",
|
|
b"X-FOOBAR;C=one;A=two;B=three:helloworld.",
|
|
b"END:VEVENT",
|
|
]
|
|
component = Component.from_ical(b"\r\n".join(component_str))
|
|
|
|
sorted_str = component.to_ical().splitlines()
|
|
assert sorted_str[0] == component_str[0]
|
|
assert sorted_str[1] == b"X-FOOBAR;A=two;B=three;C=one:helloworld."
|
|
assert sorted_str[2] == component_str[2]
|
|
|
|
preserved_str = component.to_ical(sorted=False).splitlines()
|
|
assert preserved_str == component_str
|
|
|
|
|
|
@pytest.fixture
|
|
def repr_example(c):
|
|
class ReprExample:
|
|
component = c
|
|
component["key1"] = "value1"
|
|
calendar = Calendar()
|
|
calendar["key1"] = "value1"
|
|
event = Event()
|
|
event["key1"] = "value1"
|
|
nested = Component(key1="VALUE1")
|
|
nested.add_component(component)
|
|
nested.add_component(calendar)
|
|
|
|
return ReprExample
|
|
|
|
|
|
def test_repr_component(repr_example):
|
|
"""Test correct class representation."""
|
|
assert re.match(r"Component\({u?'KEY1': u?'value1'}\)", str(repr_example.component))
|
|
|
|
|
|
def test_repr_calendar(repr_example):
|
|
assert re.match(r"VCALENDAR\({u?'KEY1': u?'value1'}\)", str(repr_example.calendar))
|
|
|
|
|
|
def test_repr_event(repr_example):
|
|
assert re.match(r"VEVENT\({u?'KEY1': u?'value1'}\)", str(repr_example.event))
|
|
|
|
|
|
def test_nested_components(repr_example):
|
|
"""Representation of nested Components"""
|
|
repr_example.calendar.add_component(repr_example.event)
|
|
print(repr_example.nested)
|
|
assert re.match(
|
|
r"Component\({u?'KEY1': u?'VALUE1'}, "
|
|
r"Component\({u?'KEY1': u?'value1'}\), "
|
|
r"VCALENDAR\({u?'KEY1': u?'value1'}, "
|
|
r"VEVENT\({u?'KEY1': u?'value1'}\)\)\)",
|
|
str(repr_example.nested),
|
|
)
|
|
|
|
|
|
def test_component_factory_VEVENT(factory):
|
|
"""Check the events in the component factory"""
|
|
component = factory["VEVENT"]
|
|
event = component(dtstart="19700101")
|
|
assert event.to_ical() == b"BEGIN:VEVENT\r\nDTSTART:19700101\r\nEND:VEVENT\r\n"
|
|
|
|
|
|
def test_component_factory_VCALENDAR(factory):
|
|
"""Check the VCALENDAR in the factory."""
|
|
assert factory.get("VCALENDAR") == icalendar.cal.Calendar
|
|
|
|
|
|
def test_minimal_calendar_component_with_one_event():
|
|
"""Setting up a minimal calendar component looks like this"""
|
|
cal = Calendar()
|
|
|
|
# Some properties are required to be compliant
|
|
cal["prodid"] = "-//My calendar product//mxm.dk//"
|
|
cal["version"] = "2.0"
|
|
|
|
# We also need at least one subcomponent for a calendar to be compliant
|
|
event = Event()
|
|
event["summary"] = "Python meeting about calendaring"
|
|
event["uid"] = "42"
|
|
event.add("dtstart", datetime(2005, 4, 4, 8, 0, 0))
|
|
cal.add_component(event)
|
|
assert (
|
|
cal.subcomponents[0].to_ical()
|
|
== b"BEGIN:VEVENT\r\nSUMMARY:Python meeting about calendaring\r\n"
|
|
+ b"DTSTART:20050404T080000\r\nUID:42\r\n"
|
|
+ b"END:VEVENT\r\n"
|
|
)
|
|
|
|
|
|
def test_calendar_with_parsing_errors_includes_all_events(calendars):
|
|
"""Parsing a complete calendar from a string will silently ignore wrong
|
|
events but adding the error information to the component's 'errors'
|
|
attribute. The error in the following is the third EXDATE: it has an
|
|
empty DATE.
|
|
"""
|
|
event_descriptions = [
|
|
e["DESCRIPTION"].to_ical() for e in calendars.parsing_error.walk("VEVENT")
|
|
]
|
|
assert event_descriptions == [b"Perfectly OK event", b"Wrong event"]
|
|
|
|
|
|
def test_calendar_with_parsing_errors_has_an_error_in_one_event(calendars):
|
|
"""Parsing a complete calendar from a string will silently ignore wrong
|
|
events but adding the error information to the component's 'errors'
|
|
attribute. The error in the following is the third EXDATE: it has an
|
|
empty DATE.
|
|
"""
|
|
errors = [e.errors for e in calendars.parsing_error.walk("VEVENT")]
|
|
assert errors == [[], [("EXDATE", "Expected datetime, date, or time, got: ''")]]
|
|
|
|
|
|
def test_cal_strict_parsing(calendars):
|
|
"""If components are damaged, we raise an exception."""
|
|
with pytest.raises(ValueError):
|
|
calendars.parsing_error_in_UTC_offset
|
|
|
|
|
|
def test_cal_ignore_errors_parsing(calendars, vUTCOffset_ignore_exceptions):
|
|
"""If we diable the errors, we should be able to put the calendar back together."""
|
|
assert (
|
|
calendars.parsing_error_in_UTC_offset.to_ical()
|
|
== calendars.parsing_error_in_UTC_offset.raw_ics
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("calendar", "other_calendar"),
|
|
itertools.product(
|
|
[
|
|
"issue_156_RDATE_with_PERIOD_TZID_khal",
|
|
"issue_156_RDATE_with_PERIOD_TZID_khal_2",
|
|
"issue_178_custom_component_contains_other",
|
|
"issue_178_custom_component_inside_other",
|
|
"issue_526_calendar_with_events",
|
|
"issue_526_calendar_with_different_events",
|
|
"issue_526_calendar_with_event_subset",
|
|
],
|
|
repeat=2,
|
|
),
|
|
)
|
|
def test_comparing_calendars(calendars, calendar, other_calendar, tzp):
|
|
are_calendars_equal = calendars[calendar] == calendars[other_calendar]
|
|
are_calendars_actually_equal = calendar == other_calendar
|
|
assert are_calendars_equal == are_calendars_actually_equal
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("calendar", "shuffeled_calendar"),
|
|
[
|
|
(
|
|
"issue_526_calendar_with_events",
|
|
"issue_526_calendar_with_shuffeled_events",
|
|
),
|
|
],
|
|
)
|
|
def test_calendars_with_same_subcomponents_in_different_order_are_equal(
|
|
calendars, calendar, shuffeled_calendar
|
|
):
|
|
assert (
|
|
calendars[calendar].subcomponents != calendars[shuffeled_calendar].subcomponents
|
|
)
|
|
assert calendars[calendar] == calendars[shuffeled_calendar]
|