diff --git a/Sources/SwiftIcal/Component.swift b/Sources/SwiftIcal/Component.swift index b563de4..35d5b5a 100644 --- a/Sources/SwiftIcal/Component.swift +++ b/Sources/SwiftIcal/Component.swift @@ -39,6 +39,17 @@ extension LibicalComponent { } return result } + + subscript(kind: icalparameter_kind)-> [LibicalParameter] { + var result: [LibicalParameter] = [] + if let first = icalproperty_get_first_parameter(self, kind) { + result.append(first) + } + while let property = icalproperty_get_next_parameter(self, kind) { + result.append(property) + } + return result + } } extension LibicalProperty { @@ -50,6 +61,37 @@ extension LibicalProperty { } } +extension LibicalComponent { + public var icalComponentString: String { + let c = self + + guard let stringPointer = icalcomponent_as_ical_string(c) else { + fatalError("Failed to get component as string") + } + defer { + icalmemory_free_buffer(stringPointer) + } + + let string = String(cString: stringPointer) + return string + } +} + +extension LibicalComponent { + public var icalPropertyString: String { + let c = self + + guard let stringPointer = icalproperty_as_ical_string(c) else { + fatalError("Failed to get property as string") + } + defer { + icalmemory_free_buffer(UnsafeMutableRawPointer(mutating: stringPointer)) + } + + let string = String(cString: stringPointer) + return string + } +} extension DateComponents { var icaltime: icaltimetype { @@ -127,7 +169,6 @@ extension CalendarUserType: LibicalPropertyConvertible { case .unknown: return icalparameter_new_cutype(ICAL_CUTYPE_UNKNOWN) } - } } @@ -171,8 +212,6 @@ extension EventParticipationStatus: LibicalParameterConvertible { return parameter! } } - - } public enum Role: Equatable { @@ -200,8 +239,6 @@ extension Role: LibicalParameterConvertible { return parameter! } } - - } @@ -217,7 +254,8 @@ public struct Attendee { delegatedTo: [CalendarUserAddress]? = nil, delegatedFrom: [CalendarUserAddress]? = nil, sentBy: CalendarUserAddress? = nil, - commonName: CommonName? = nil) { + commonName: CommonName? = nil, + xParameters: [String: String] = [:]) { self.address = address self.type = type self.participationStatus = participationStatus @@ -228,6 +266,7 @@ public struct Attendee { self.delegatedFrom = delegatedFrom self.sentBy = sentBy self.commonName = commonName + self.xParameters = xParameters } /// E-Mail address of the attendee @@ -241,23 +280,23 @@ public struct Attendee { /// Role of the attendee, the default value is `.requiredParticipant` public var role: Role = .requiredParticipant - + /// Group membership. /// /// See [RFC 5545 Section 3.2.11](https://tools.ietf.org/html/rfc5545#section-3.2.11) for more details public var member: CalendarUserAddress? - - /// List for users the attendance has been delegated to + + /// List for users the attendance has been delegated to. /// /// See [RFC 5545 Section 3.2.4](https://tools.ietf.org/html/rfc5545#section-3.2.5) for more details public var delegatedTo: [CalendarUserAddress]? - - /// List for users the attendance has been delegated from + + /// List for users the attendance has been delegated from. /// /// See [RFC 5545 Section 3.2.4](https://tools.ietf.org/html/rfc5545#section-3.2.4) for more details public var delegatedFrom: [CalendarUserAddress]? - - /// User that has sent the invitation + + /// User that has sent the invitation. /// /// See [RFC 5545 Section 3.2.18](https://tools.ietf.org/html/rfc5545#section-3.2.18) for more details public var sentBy: CalendarUserAddress? @@ -268,6 +307,9 @@ public struct Attendee { /// Property if the attendee is requested to send public var rsvp: Bool + + /// Custom X parameters + public var xParameters: [String: String] = [:] } extension Attendee: LibicalPropertyConvertible { @@ -276,6 +318,10 @@ extension Attendee: LibicalPropertyConvertible { if type != .individual { icalproperty_add_parameter(property, type.libicalProperty()) } + + if let member = member { + icalproperty_add_parameter(property, icalparameter_new_member(member)) + } if participationStatus != .needsAction { icalproperty_add_parameter(property, participationStatus.libicalParameter()) @@ -306,6 +352,12 @@ extension Attendee: LibicalPropertyConvertible { if rsvp == true { icalproperty_add_parameter(property, icalparameter_new_rsvp(ICAL_RSVP_TRUE)) } + + for xParameter in xParameters { + let param = icalparameter_new_from_string("\(xParameter.key)=\(xParameter.value)") + icalproperty_add_parameter(property, param) + } + return property! } } @@ -318,25 +370,34 @@ public struct Organizer { self.commonName = commonName self.sentBy = sentBy } - - + /// E-Mail address of the Organizer public var address: CalendarUserAddress /// Name of the Organizer public var commonName: CommonName? public var sentBy: CalendarUserAddress? + + /// Custom X parameters + public var xParameters: [String: String] = [:] } extension Organizer: LibicalPropertyConvertible { func libicalProperty() -> LibicalProperty { - let property = icalproperty_new_organizer(self.address) + let property = icalproperty_new_organizer(self.address.mailtoAddress) + if let commonName = commonName { icalproperty_add_parameter(property, icalparameter_new_cn(commonName)) } if let sentBy = sentBy { - icalproperty_add_parameter(property, icalparameter_new_sentby(sentBy)) + icalproperty_add_parameter(property, icalparameter_new_sentby(sentBy.mailtoAddress)) } + + for xParameter in xParameters { + let param = icalparameter_new_from_string("\(xParameter.key)=\(xParameter.value)") + icalproperty_add_parameter(property, param) + } + return property! } } @@ -356,8 +417,6 @@ extension Transparency: LibicalPropertyConvertible { return icalproperty_new_transp(ICAL_TRANSP_TRANSPARENT) } } - - } public struct VEvent { @@ -367,6 +426,8 @@ public struct VEvent { self.dtstart = dtstart self.dtend = dtend } + + public var useTZIDPrefix = true /// A short summary or subject for the calendar component. /// @@ -387,16 +448,21 @@ public struct VEvent { public var uid: String = UUID().uuidString public var created: Date = Date() - + public var recurranceRule: RecurranceRule? - - public var duration: TimeInterval? + + public var duration: Duration? public var attendees: [Attendee]? = nil public var organizer: Organizer? = nil public var transparency: Transparency = .opaque + + public var alarm: VAlarm? = nil + + /// Custom X properties + public var xProperties: [String: String] = [:] } extension VEvent: LibicalComponentConvertible { @@ -409,17 +475,30 @@ extension VEvent: LibicalComponentConvertible { let dtstartProperty = icalproperty_new_dtstart(dtstart.date!.icalTime(timeZone: dtstart.timeZone ?? .utc)) if let timezone = dtstart.timeZone { - icalproperty_add_parameter(dtstartProperty, icalparameter_new_tzid(String(cString: icaltimezone_tzid_prefix()!) + timezone.identifier)) + if useTZIDPrefix { + icalproperty_add_parameter(dtstartProperty, icalparameter_new_tzid(String(cString: icaltimezone_tzid_prefix()!) + timezone.identifier)) + } else { + icalproperty_add_parameter(dtstartProperty, icalparameter_new_tzid(timezone.identifier)) + } } icalcomponent_add_property(comp, dtstartProperty) if let dtend = dtend { let dtendProperty = icalproperty_new_dtend(dtend.date!.icalTime(timeZone: dtend.timeZone ?? .utc)) if let timezone = dtend.timeZone { - icalproperty_add_parameter(dtendProperty, icalparameter_new_tzid(String(cString: icaltimezone_tzid_prefix()!) + timezone.identifier)) + if useTZIDPrefix { + icalproperty_add_parameter(dtendProperty, icalparameter_new_tzid(String(cString: icaltimezone_tzid_prefix()!) + timezone.identifier)) + } else { + icalproperty_add_parameter(dtendProperty, icalparameter_new_tzid(timezone.identifier)) + } } icalcomponent_add_property(comp, dtendProperty) } + + if let duration = duration { + let duration = icaldurationtype(is_neg: 0, days: UInt32(duration.days), weeks: UInt32(duration.weeks), hours: UInt32(duration.hours), minutes: UInt32(duration.minutes), seconds: UInt32(duration.seconds)) + icalcomponent_add_property(comp, icalproperty_new_duration(duration)) + } icalcomponent_add_property(comp, icalproperty_new_summary(summary)) icalcomponent_add_property(comp, icalproperty_new_uid(uid)) @@ -437,7 +516,100 @@ extension VEvent: LibicalComponentConvertible { if let organizer = organizer { icalcomponent_add_property(comp, organizer.libicalProperty()) } + + if let alarm = alarm { + icalcomponent_add_component(comp, alarm.libicalComponent()) + } + + for xProperty in xProperties { + let param = icalproperty_new_x(xProperty.value) + icalproperty_set_x_name(param, xProperty.key) + icalcomponent_add_property(comp, param) + } + return comp! } } +public struct Duration { + public let seconds: Int + public let minutes: Int + public let hours: Int + public let days: Int + public let weeks: Int + + public init(seconds: Int, minutes: Int, hours: Int, days: Int, weeks: Int) { + self.seconds = seconds + self.minutes = minutes + self.hours = hours + self.days = days + self.weeks = weeks + } +} + +public enum AlarmTrigger { + case duration(duration: Duration) + case time(date: DateComponents) +} + +public struct AlarmFrequent { + let frequent: Int + let duration: Duration +} + +public enum AlarmAction { + case display(description: String) + case email(summary: String, description: String, attendees: [Attendee]) +} + +public struct VAlarm { + public var trigger: AlarmTrigger + public var action: AlarmAction + public var frequent: AlarmFrequent? + + public init(trigger: AlarmTrigger, action: AlarmAction) { + self.trigger = trigger + self.action = action + } +} + +extension VAlarm: LibicalComponentConvertible { + func libicalComponent() -> LibicalComponent { + let alarm = icalcomponent_new_valarm() + + var triggertype = icaltriggertype() + switch trigger { + case .duration(duration: let duration): + let duration = icaldurationtype(is_neg: 1, days: UInt32(duration.days), weeks: UInt32(duration.weeks), hours: UInt32(duration.hours), minutes: UInt32(duration.minutes), seconds: UInt32(duration.seconds)) + triggertype.duration = duration + triggertype.time = icaltime_null_time() + case .time(date: let date): + triggertype.duration = icaldurationtype_null_duration() + triggertype.time = date.date!.icalTime(timeZone: date.timeZone ?? .utc) + } + + let trigger = icalproperty_new_trigger(triggertype) + icalcomponent_add_property(alarm, trigger) + + switch action { + case .display(description: let description): + icalcomponent_add_property(alarm, icalproperty_new_action(ICAL_ACTION_DISPLAY)); + icalcomponent_add_property(alarm, icalproperty_new_description(description)); + case .email(summary: let summary, description: let description, attendees: let attendees): + icalcomponent_add_property(alarm, icalproperty_new_action(ICAL_ACTION_EMAIL)); + icalcomponent_add_property(alarm, icalproperty_new_summary(summary)); + icalcomponent_add_property(alarm, icalproperty_new_description(description)); + attendees.forEach({ (attendee) in + icalcomponent_add_property(alarm, attendee.libicalProperty()) + }) + } + + if let frequent = frequent { + let duration = icaldurationtype(is_neg: 0, days: UInt32(frequent.duration.days), weeks: UInt32(frequent.duration.weeks), hours: UInt32(frequent.duration.hours), minutes: UInt32(frequent.duration.minutes), seconds: UInt32(frequent.duration.seconds)) + icalcomponent_add_property(alarm, icalproperty_new_duration(duration)) + icalcomponent_add_property(alarm, icalproperty_new_repeat(Int32(frequent.frequent))); + } + + return alarm! + } +} diff --git a/Sources/SwiftIcal/Types.swift b/Sources/SwiftIcal/Types.swift index 7161a78..17043bf 100644 --- a/Sources/SwiftIcal/Types.swift +++ b/Sources/SwiftIcal/Types.swift @@ -43,24 +43,19 @@ extension TimeZone { return icaltimezone_get_builtin_timezone_from_tzid(self.identifier) } - var icalComponent: LibicalComponent { + func icalComponent(useTZIDPrefix: Bool) -> LibicalComponent? { loadZones() - + let tz = icaltimezone_get_builtin_timezone_from_tzid("/freeassociation.sourceforge.net/" + self.identifier) let comp = icaltimezone_get_component(tz) - return comp! - } - - public var icalString: String { - guard let stringPointer = icalcomponent_as_ical_string(icalComponent) else { - fatalError("Failed to get component as string") + if !useTZIDPrefix { + let tzidProperty = icalcomponent_get_first_property(comp, ICAL_TZID_PROPERTY) + icalproperty_set_tzid(tzidProperty, self.identifier) } - let string = String(cString: stringPointer) - icalmemory_free_buffer(stringPointer) - return string + + return comp } - } diff --git a/Sources/SwiftIcal/VCalendar.swift b/Sources/SwiftIcal/VCalendar.swift index cc37324..7d4f4db 100644 --- a/Sources/SwiftIcal/VCalendar.swift +++ b/Sources/SwiftIcal/VCalendar.swift @@ -62,6 +62,8 @@ public struct VCalendar { /// If `autoincludeTimezones` is enabled, timezone definitions for all /// timezones used in `vevents` will be added to the `VCALENDAR` output. public var autoincludeTimezones = true + + public var useTZIDPrefix = true /// Creates a new `VCALENDAR` public init() {} @@ -69,8 +71,7 @@ public struct VCalendar { /// Returns the `VCALENDAR` representation as a string public func icalString() -> String { let c = component() - - //defer { icalmemory_free_buffer(c) } + guard let stringPointer = icalcomponent_as_ical_string(c) else { fatalError("Failed to get component as string") } @@ -79,6 +80,7 @@ public struct VCalendar { let string = String(cString: stringPointer) return string } + /// Serializes the the component into a libical structure. /// It's the callers responsibliy to call `icalcomponent_free` to free @@ -102,10 +104,9 @@ public struct VCalendar { } } } - - + allTimezones.forEach { (timezone) in - icalcomponent_add_component(calendar, timezone.icalComponent) + icalcomponent_add_component(calendar, timezone.icalComponent(useTZIDPrefix: useTZIDPrefix)) } events.forEach { event in icalcomponent_add_component(calendar, event.libicalComponent()) @@ -117,17 +118,16 @@ public struct VCalendar { public enum ParseError: Error, Equatable { case invalidVCalendar - case invalidVersion - case noVersion + case missingProperty(icalproperty_kind) + case invalidProperty(icalproperty_kind) } - - extension VCalendar { public static func parse(_ string: String) throws -> VCalendar { guard let calendarComponent: LibicalComponent = icalcomponent_new_from_string(string) else { throw ParseError.invalidVCalendar } + var calendar = VCalendar() // Parse Prodid if let prodid = calendarComponent[ICAL_PRODID_PROPERTY].first?.value { @@ -136,10 +136,11 @@ extension VCalendar { // Parse Version guard let version = calendarComponent[ICAL_VERSION_PROPERTY].first?.value else { - throw ParseError.noVersion + throw ParseError.missingProperty(ICAL_VERSION_PROPERTY) } + if version != "2.0" { - throw ParseError.invalidVersion + throw ParseError.invalidProperty(ICAL_VERSION_PROPERTY) } // Parse Method @@ -147,7 +148,6 @@ extension VCalendar { if let methodProperty = calendarComponent[ICAL_METHOD_PROPERTY].first { calendar.method = Method.from(property: methodProperty) } - return calendar } diff --git a/Tests/SwiftIcalTests/AlarmTests.swift b/Tests/SwiftIcalTests/AlarmTests.swift new file mode 100644 index 0000000..d195f16 --- /dev/null +++ b/Tests/SwiftIcalTests/AlarmTests.swift @@ -0,0 +1,130 @@ +// +// AlarmTests.swift +// +// +// Created by Blažej Brezoňák on 05/03/2022. +// + +import XCTest +import CLibical +import Foundation +@testable import SwiftIcal + +class AlarmTests: XCTestCase { + func testAlertDisplayDuration() { + let alarm = VAlarm(trigger: .duration(duration: Duration(seconds: 0, minutes: 30, hours: 0, days: 0, weeks: 0)), action: .display(description: "Remind me")) + let alarmComponent = alarm.libicalComponent().icalComponentString + let expected = """ + BEGIN:VALARM + TRIGGER:-PT30M + ACTION:DISPLAY + DESCRIPTION:Remind me + END:VALARM + """ + AssertICSEqual(alarmComponent, expected.icalFormatted) + } + + func testAlertDisplayDate() { + let alarm = VAlarm(trigger: .time(date: .testDate(year: 2023, month: 5, day: 9, hour: 11, minute: 0, second: 0)), action: .display(description: "Remind me")) + let alarmComponent = alarm.libicalComponent().icalComponentString + let expected = """ + BEGIN:VALARM + TRIGGER;VALUE=DATE-TIME:20230509T110000 + ACTION:DISPLAY + DESCRIPTION:Remind me + END:VALARM + """ + AssertICSEqual(alarmComponent, expected.icalFormatted) + } + + func testAlertEmailDuration() { + let alarm = VAlarm(trigger: .duration(duration: Duration(seconds: 30, minutes: 30, hours: 2, days: 1, weeks: 1)), action: .email(summary: "summary", description: "description", attendees: [Attendee(address: "test@test.com"), Attendee(address: "test2@test.com")])) + let alarmComponent = alarm.libicalComponent().icalComponentString + let expected = """ + BEGIN:VALARM + TRIGGER:-P1W1DT2H30M30S + ACTION:EMAIL + SUMMARY:summary + DESCRIPTION:description + ATTENDEE:mailto:test@test.com + ATTENDEE:mailto:test2@test.com + END:VALARM + """ + AssertICSEqual(alarmComponent, expected.icalFormatted) + } + + func testAlertEmailDate() { + let alarm = VAlarm(trigger: .time(date: .testDate(year: 2023, month: 5, day: 9, hour: 11, minute: 0, second: 0)), action: .email(summary: "summary", description: "description", attendees: [Attendee(address: "test@test.com"), Attendee(address: "test2@test.com")])) + let alarmComponent = alarm.libicalComponent().icalComponentString + let expected = """ + BEGIN:VALARM + TRIGGER;VALUE=DATE-TIME:20230509T110000 + ACTION:EMAIL + SUMMARY:summary + DESCRIPTION:description + ATTENDEE:mailto:test@test.com + ATTENDEE:mailto:test2@test.com + END:VALARM + """ + AssertICSEqual(alarmComponent, expected.icalFormatted) + } + + func testAlertDisplayDurationRepeat() { + var alarm = VAlarm(trigger: .duration(duration: Duration(seconds: 0, minutes: 30, hours: 0, days: 0, weeks: 0)), action: .display(description: "Remind me")) + alarm.frequent = AlarmFrequent(frequent: 25, duration: Duration(seconds: 0, minutes: 30, hours: 0, days: 0, weeks: 0)) + let alarmComponent = alarm.libicalComponent().icalComponentString + let expected = """ + BEGIN:VALARM + TRIGGER:-PT30M + ACTION:DISPLAY + DESCRIPTION:Remind me + DURATION:PT30M + REPEAT:25 + END:VALARM + """ + AssertICSEqual(alarmComponent, expected.icalFormatted) + } + + func testAlertInEvent() { + var event = VEvent(summary: "Hello World", + dtstart: .testDate(year: 2020, month: 5, day: 9, hour: 11, minute: 0, second: 0)) + event.dtend = .testDate(year: 2020, month: 5, day: 9, hour: 12, minute: 0, second: 0) + event.dtstamp = Date(timeIntervalSince1970: 0) + event.created = Date(timeIntervalSince1970: 0) + event.uid = "TEST-UID" + + var alarm = VAlarm(trigger: .duration(duration: Duration(seconds: 0, minutes: 30, hours: 0, days: 0, weeks: 0)), action: .display(description: "Remind me")) + alarm.frequent = AlarmFrequent(frequent: 25, duration: Duration(seconds: 0, minutes: 30, hours: 0, days: 0, weeks: 0)) + event.alarm = alarm + + var calendar = VCalendar() + calendar.events.append(event) + calendar.autoincludeTimezones = false + + let expected = """ + BEGIN:VCALENDAR + PRODID:-//SwiftIcal/EN + VERSION:2.0 + BEGIN:VEVENT + DTSTAMP:19700101T000000Z + DTSTART;TZID=/freeassociation.sourceforge.net/Europe/Berlin: + 20200509T110000 + DTEND;TZID=/freeassociation.sourceforge.net/Europe/Berlin: + 20200509T120000 + SUMMARY:Hello World + UID:TEST-UID + TRANSP:OPAQUE + CREATED:19700101T000000Z + BEGIN:VALARM + TRIGGER:-PT30M + ACTION:DISPLAY + DESCRIPTION:Remind me + DURATION:PT30M + REPEAT:25 + END:VALARM + END:VEVENT + END:VCALENDAR + """ + AssertICSEqual(calendar.icalString(), expected.icalFormatted) + } +} diff --git a/Tests/SwiftIcalTests/AttendeeTests.swift b/Tests/SwiftIcalTests/AttendeeTests.swift new file mode 100644 index 0000000..1648082 --- /dev/null +++ b/Tests/SwiftIcalTests/AttendeeTests.swift @@ -0,0 +1,263 @@ +// +// AttendeeTests.swift +// +// +// Created by Blažej Brezoňák on 12/03/2022. +// + +import XCTest +@testable import SwiftIcal + + +class AttendeeTests: XCTestCase { + func testAttendeeMail() { + let attendee = Attendee(address: "thomas@bartelmess.io") + let expected = """ + ATTENDEE:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeMailName() { + let attendee = Attendee(address: "thomas@bartelmess.io", commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeMailNameXParameter() { + var attendee = Attendee(address: "thomas@bartelmess.io", commonName: "Thomas Bartelmess") + attendee.xParameters = ["X-NAME": "1"] + let expected = """ + ATTENDEE;CN=Thomas Bartelmess;X-NAME=1:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeMailNameXParameters() { + var attendee = Attendee(address: "thomas@bartelmess.io", commonName: "Thomas Bartelmess") + let xParameters = ["X-NAME": "1", "X-NAME2": "0"] + attendee.xParameters = xParameters + let attendeePropertyString = attendee.libicalProperty().icalPropertyString + + for xParameter in xParameters { + if !attendeePropertyString.contains("\(xParameter.key)=\(xParameter.value)") { + XCTFail() + } + } + } + + func testAttendeeTypeIndividual() { + let attendee = Attendee(address: "thomas@bartelmess.io", type: .individual, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeTypeRoom() { + let attendee = Attendee(address: "thomas@bartelmess.io", type: .room, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;CUTYPE=ROOM;CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeTypeGroup() { + let attendee = Attendee(address: "thomas@bartelmess.io", type: .group, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;CUTYPE=GROUP;CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeTypeResource() { + let attendee = Attendee(address: "thomas@bartelmess.io", type: .resource, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;CUTYPE=RESOURCE;CN=Thomas Bartelmess:mailto: + thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeTypeUnknown() { + let attendee = Attendee(address: "thomas@bartelmess.io", type: .unknown, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;CUTYPE=UNKNOWN;CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeTypeX() { + let attendee = Attendee(address: "thomas@bartelmess.io", type: .x("TEST"), commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;CUTYPE=TEST;CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeParticipationStatusNeedsAction() { + let attendee = Attendee(address: "thomas@bartelmess.io", participationStatus: .needsAction, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeParticipationStatusAccepted() { + let attendee = Attendee(address: "thomas@bartelmess.io", participationStatus: .accepted, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;PARTSTAT=ACCEPTED;CN=Thomas Bartelmess:mailto: + thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeParticipationStatusDeclined() { + let attendee = Attendee(address: "thomas@bartelmess.io", participationStatus: .decliend, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;PARTSTAT=DECLINED;CN=Thomas Bartelmess:mailto: + thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeParticipationStatusTentative() { + let attendee = Attendee(address: "thomas@bartelmess.io", participationStatus: .tentative, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;PARTSTAT=TENTATIVE;CN=Thomas Bartelmess:mailto: + thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeParticipationStatusDelegated() { + let attendee = Attendee(address: "thomas@bartelmess.io", participationStatus: .delegated, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;PARTSTAT=DELEGATED;CN=Thomas Bartelmess:mailto: + thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeParticipationStatusX() { + let attendee = Attendee(address: "thomas@bartelmess.io", participationStatus: .x("TEST"), commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;PARTSTAT=TEST;CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeRoleRequiredParticipant() { + let attendee = Attendee(address: "thomas@bartelmess.io", role: .requiredParticipant, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeRoleChair() { + let attendee = Attendee(address: "thomas@bartelmess.io", role: .chair, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;ROLE=CHAIR;CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeRoleOptionalParticipant() { + let attendee = Attendee(address: "thomas@bartelmess.io", role: .optionalParticipant, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;ROLE=OPT-PARTICIPANT;CN=Thomas Bartelmess:mailto: + thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeRoleNonParticipant() { + let attendee = Attendee(address: "thomas@bartelmess.io", role: .nonParticipant, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;ROLE=NON-PARTICIPANT;CN=Thomas Bartelmess:mailto: + thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeRoleX() { + let attendee = Attendee(address: "thomas@bartelmess.io", role: .x("TEST"), commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;ROLE=TEST;CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeRSVPTrue() { + let attendee = Attendee(address: "thomas@bartelmess.io", rsvp: true, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;CN=Thomas Bartelmess;RSVP=TRUE:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeRSVPFalse() { + let attendee = Attendee(address: "thomas@bartelmess.io", rsvp: false, commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeMember() { + let attendee = Attendee(address: "thomas@bartelmess.io", member: "member@member.io", commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;MEMBER=member@member.io;CN=Thomas Bartelmess:mailto: + thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeDelegatedTo() { + let attendee = Attendee(address: "thomas@bartelmess.io", delegatedTo: ["delegat@delwegate.io"], commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;DELEGATED-TO=delegat@delwegate.io;CN=Thomas Bartelmess:mailto: + thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeDelegatedTo2() { + let attendee = Attendee(address: "thomas@bartelmess.io", delegatedTo: ["delegat@delwegate.io", "delegat2@delwegate.io"], commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;DELEGATED-TO="delegat@delwegate.io,delegat2@delwegate.io"; + CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeDelegatedFrom() { + let attendee = Attendee(address: "thomas@bartelmess.io", delegatedFrom: ["delegat@delwegate.io"], commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;DELEGATED-FROM=delegat@delwegate.io;CN=Thomas Bartelmess:mailto: + thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeDelegatedFrom2() { + let attendee = Attendee(address: "thomas@bartelmess.io", delegatedFrom: ["delegat@delwegate.io", "delegat2@delwegate.io"], commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;DELEGATED-FROM="delegat@delwegate.io,delegat2@delwegate.io"; + CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } + + func testAttendeeSendBy() { + let attendee = Attendee(address: "thomas@bartelmess.io", sentBy: "sent@by.io", commonName: "Thomas Bartelmess") + let expected = """ + ATTENDEE;SENT-BY=sent@by.io;CN=Thomas Bartelmess:mailto: + thomas@bartelmess.io + """ + XCTAssertEqual(attendee.libicalProperty().icalPropertyString, expected.icalFormatted) + } +} diff --git a/Tests/SwiftIcalTests/EventTests.swift b/Tests/SwiftIcalTests/EventTests.swift index 72a6b06..c11efe6 100644 --- a/Tests/SwiftIcalTests/EventTests.swift +++ b/Tests/SwiftIcalTests/EventTests.swift @@ -154,4 +154,167 @@ class EventTests: XCTestCase { """.icalFormatted XCTAssertEqual(calendar.icalString(), expected) } + + func testEventNotUseTZIDPrefix() { + var event = VEvent(summary: "Hello World", + dtstart: .testDate(year: 2020, month: 5, day: 9, hour: 11, minute: 0, second: 0)) + event.dtend = .testDate(year: 2020, month: 5, day: 9, hour: 12, minute: 0, second: 0) + event.dtstamp = Date(timeIntervalSince1970: 0) + event.created = Date(timeIntervalSince1970: 0) + event.uid = "TEST-UID" + event.useTZIDPrefix = false + var calendar = VCalendar() + calendar.events.append(event) + calendar.autoincludeTimezones = false + let expected = """ + BEGIN:VCALENDAR + PRODID:-//SwiftIcal/EN + VERSION:2.0 + BEGIN:VEVENT + DTSTAMP:19700101T000000Z + DTSTART;TZID=Europe/Berlin:20200509T110000 + DTEND;TZID=Europe/Berlin:20200509T120000 + SUMMARY:Hello World + UID:TEST-UID + TRANSP:OPAQUE + CREATED:19700101T000000Z + END:VEVENT + END:VCALENDAR + """ + AssertICSEqual(calendar.icalString(), expected.icalFormatted) + } + + func testEventXProperties() { + var event = VEvent(summary: "Hello World", + dtstart: .testDate(year: 2020, month: 5, day: 9, hour: 11, minute: 0, second: 0)) + event.dtend = .testDate(year: 2020, month: 5, day: 9, hour: 12, minute: 0, second: 0) + event.dtstamp = Date(timeIntervalSince1970: 0) + event.created = Date(timeIntervalSince1970: 0) + event.uid = "TEST-UID" + event.xProperties = ["X-NAME": "value", "X-NAME-2": "value2"] + var calendar = VCalendar() + calendar.events.append(event) + calendar.autoincludeTimezones = false + let expected = """ + BEGIN:VCALENDAR + PRODID:-//SwiftIcal/EN + VERSION:2.0 + BEGIN:VEVENT + DTSTAMP:19700101T000000Z + DTSTART;TZID=/freeassociation.sourceforge.net/Europe/Berlin: + 20200509T110000 + DTEND;TZID=/freeassociation.sourceforge.net/Europe/Berlin: + 20200509T120000 + SUMMARY:Hello World + UID:TEST-UID + TRANSP:OPAQUE + CREATED:19700101T000000Z + X-NAME-2:value2 + X-NAME:value + END:VEVENT + END:VCALENDAR + """ + AssertICSEqual(calendar.icalString(), expected.icalFormatted) + } + + func testEventWithOrganizer() { + var event = VEvent(summary: "Hello World", dtstart: .testDate(year: 2020, month: 5, day: 9, hour: 11, minute: 0, second: 0)) + event.dtend = .testDate(year: 2020, month: 5, day: 9, hour: 12, minute: 0, second: 0) + event.attendees = [Attendee(address: "thomas@bartelmess.io", commonName: "Thomas Bartelmess")] + event.organizer = Organizer(address: "organizer@test.com", commonName: "Organizer Name", sentBy: nil) + event.dtstamp = Date(timeIntervalSince1970: 0) + event.created = Date(timeIntervalSince1970: 0) + event.uid = "TEST-UID" + var calendar = VCalendar() + calendar.events.append(event) + calendar.autoincludeTimezones = false + + let expected = """ + BEGIN:VCALENDAR + PRODID:-//SwiftIcal/EN + VERSION:2.0 + BEGIN:VEVENT + DTSTAMP:19700101T000000Z + DTSTART;TZID=/freeassociation.sourceforge.net/Europe/Berlin: + 20200509T110000 + DTEND;TZID=/freeassociation.sourceforge.net/Europe/Berlin: + 20200509T120000 + SUMMARY:Hello World + UID:TEST-UID + TRANSP:OPAQUE + CREATED:19700101T000000Z + ATTENDEE;CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + ORGANIZER;CN=Organizer Name:mailto:organizer@test.com + END:VEVENT + END:VCALENDAR + """.icalFormatted + XCTAssertEqual(calendar.icalString(), expected) + } + + func testEventWithAttendeesWithXParameters() { + var event = VEvent(summary: "Hello World", dtstart: .testDate(year: 2020, month: 5, day: 9, hour: 11, minute: 0, second: 0)) + event.dtend = .testDate(year: 2020, month: 5, day: 9, hour: 12, minute: 0, second: 0) + var attendee = Attendee(address: "thomas@bartelmess.io", commonName: "Thomas Bartelmess") + attendee.xParameters = ["X-NAME": "1"] + event.attendees = [attendee] + event.dtstamp = Date(timeIntervalSince1970: 0) + event.created = Date(timeIntervalSince1970: 0) + event.uid = "TEST-UID" + var calendar = VCalendar() + calendar.events.append(event) + calendar.autoincludeTimezones = false + + let expected = """ + BEGIN:VCALENDAR + PRODID:-//SwiftIcal/EN + VERSION:2.0 + BEGIN:VEVENT + DTSTAMP:19700101T000000Z + DTSTART;TZID=/freeassociation.sourceforge.net/Europe/Berlin: + 20200509T110000 + DTEND;TZID=/freeassociation.sourceforge.net/Europe/Berlin: + 20200509T120000 + SUMMARY:Hello World + UID:TEST-UID + TRANSP:OPAQUE + CREATED:19700101T000000Z + ATTENDEE;CN=Thomas Bartelmess;X-NAME=1:mailto:thomas@bartelmess.io + END:VEVENT + END:VCALENDAR + """.icalFormatted + XCTAssertEqual(calendar.icalString(), expected) + } + + func testEventWithDuration() { + var event = VEvent(summary: "Hello World", dtstart: .testDate(year: 2020, month: 5, day: 9, hour: 11, minute: 0, second: 0)) + event.duration = Duration(seconds: 0, minutes: 0, hours: 1, days: 0, weeks: 0) + event.attendees = [Attendee(address: "thomas@bartelmess.io", commonName: "Thomas Bartelmess")] + event.organizer = Organizer(address: "organizer@test.com", commonName: "Organizer Name", sentBy: nil) + event.dtstamp = Date(timeIntervalSince1970: 0) + event.created = Date(timeIntervalSince1970: 0) + event.uid = "TEST-UID" + var calendar = VCalendar() + calendar.events.append(event) + calendar.autoincludeTimezones = false + + let expected = """ + BEGIN:VCALENDAR + PRODID:-//SwiftIcal/EN + VERSION:2.0 + BEGIN:VEVENT + DTSTAMP:19700101T000000Z + DTSTART;TZID=/freeassociation.sourceforge.net/Europe/Berlin: + 20200509T110000 + DURATION:PT1H + SUMMARY:Hello World + UID:TEST-UID + TRANSP:OPAQUE + CREATED:19700101T000000Z + ATTENDEE;CN=Thomas Bartelmess:mailto:thomas@bartelmess.io + ORGANIZER;CN=Organizer Name:mailto:organizer@test.com + END:VEVENT + END:VCALENDAR + """.icalFormatted + XCTAssertEqual(calendar.icalString(), expected) + } } diff --git a/Tests/SwiftIcalTests/RecurranceTests.swift b/Tests/SwiftIcalTests/RecurranceTests.swift index bbca297..0482bb4 100644 --- a/Tests/SwiftIcalTests/RecurranceTests.swift +++ b/Tests/SwiftIcalTests/RecurranceTests.swift @@ -9,6 +9,7 @@ import XCTest import Foundation import CLibical @testable import SwiftIcal + extension LibicalProperty { var icalString: String { let stringPointer = icalproperty_as_ical_string(self)! @@ -20,6 +21,7 @@ extension LibicalProperty { return string } } + extension RecurranceRule { var icalString: String { libicalProperty().icalString diff --git a/Tests/SwiftIcalTests/TimezoneTests.swift b/Tests/SwiftIcalTests/TimezoneTests.swift index 52c830e..9cca2b5 100644 --- a/Tests/SwiftIcalTests/TimezoneTests.swift +++ b/Tests/SwiftIcalTests/TimezoneTests.swift @@ -13,11 +13,11 @@ import Foundation class TimezoneTests: XCTestCase { func testSouthGeoriga() { - XCTAssertNotNil(TimeZone(identifier: "Atlantic/South_Georgia")?.icalString) + XCTAssertNotNil(TimeZone(identifier: "Atlantic/South_Georgia")?.icalComponent(useTZIDPrefix: true)?.icalComponentString) } func testTZID() { - let string = TimeZone(identifier: "America/Santiago")?.icalString + let string = TimeZone(identifier: "America/Santiago")?.icalComponent(useTZIDPrefix: true)?.icalComponentString XCTAssertTrue(string?.contains("TZID:/freeassociation.sourceforge.net/America/Santiago") ?? false) } diff --git a/Tests/SwiftIcalTests/VCalendarTests.swift b/Tests/SwiftIcalTests/VCalendarTests.swift index d18ba10..040e7ea 100644 --- a/Tests/SwiftIcalTests/VCalendarTests.swift +++ b/Tests/SwiftIcalTests/VCalendarTests.swift @@ -7,7 +7,7 @@ import XCTest import SwiftIcal - +import CLibical class VCalendarTests: XCTestCase { func testEmptyCalendar() { @@ -82,7 +82,7 @@ class VCalendarTests: XCTestCase { XCTFail("Expected error to be a ParseError. Got \(error.self)") return } - XCTAssertEqual(parseError, ParseError.noVersion) + XCTAssertEqual(parseError, ParseError.missingProperty(ICAL_VERSION_PROPERTY)) } } @@ -99,7 +99,7 @@ class VCalendarTests: XCTestCase { XCTFail("Expected error to be a ParseError. Got \(error.self)") return } - XCTAssertEqual(parseError, ParseError.invalidVersion) + XCTAssertEqual(parseError, ParseError.invalidProperty(ICAL_VERSION_PROPERTY)) } }