148 lines
4.4 KiB
Swift
148 lines
4.4 KiB
Swift
import Testing
|
|
import Foundation
|
|
@testable import TaskStore
|
|
|
|
@Suite("VTODOParser")
|
|
struct VTODOParserTests {
|
|
|
|
// MARK: - Parsing
|
|
|
|
@Test("parses all standard VTODO properties")
|
|
func parseAllProperties() {
|
|
let ics = """
|
|
BEGIN:VCALENDAR\r
|
|
VERSION:2.0\r
|
|
BEGIN:VTODO\r
|
|
UID:task-001\r
|
|
SUMMARY:Buy groceries\r
|
|
DESCRIPTION:Milk\\, eggs\\, bread\r
|
|
STATUS:NEEDS-ACTION\r
|
|
PRIORITY:5\r
|
|
DUE;VALUE=DATE:20260315\r
|
|
DTSTART;VALUE=DATE:20260310\r
|
|
CATEGORIES:errands,shopping\r
|
|
ATTACH:mid:abc123@example.com\r
|
|
X-MAGNUM-SOMEDAY:TRUE\r
|
|
CREATED:20260301T090000Z\r
|
|
LAST-MODIFIED:20260305T120000Z\r
|
|
END:VTODO\r
|
|
END:VCALENDAR\r
|
|
"""
|
|
|
|
let props = VTODOParser.parse(ics)
|
|
#expect(props != nil)
|
|
#expect(props?["UID"] == "task-001")
|
|
#expect(props?["SUMMARY"] == "Buy groceries")
|
|
#expect(props?["DESCRIPTION"] == "Milk, eggs, bread")
|
|
#expect(props?["STATUS"] == "NEEDS-ACTION")
|
|
#expect(props?["PRIORITY"] == "5")
|
|
#expect(props?["DUE"] == "20260315")
|
|
#expect(props?["DTSTART"] == "20260310")
|
|
#expect(props?["CATEGORIES"] == "errands,shopping")
|
|
#expect(props?["ATTACH"] == "mid:abc123@example.com")
|
|
#expect(props?["X-MAGNUM-SOMEDAY"] == "TRUE")
|
|
#expect(props?["CREATED"] == "20260301T090000Z")
|
|
#expect(props?["LAST-MODIFIED"] == "20260305T120000Z")
|
|
}
|
|
|
|
@Test("returns nil for invalid content without VTODO")
|
|
func invalidContentReturnsNil() {
|
|
let ics = "BEGIN:VCALENDAR\nVERSION:2.0\nEND:VCALENDAR"
|
|
#expect(VTODOParser.parse(ics) == nil)
|
|
}
|
|
|
|
@Test("returns nil for empty string")
|
|
func emptyStringReturnsNil() {
|
|
#expect(VTODOParser.parse("") == nil)
|
|
}
|
|
|
|
@Test("handles line unfolding")
|
|
func lineUnfolding() {
|
|
let ics = "BEGIN:VTODO\r\nSUMMARY:This is a very lo\r\n ng summary that was folded\r\nEND:VTODO"
|
|
let props = VTODOParser.parse(ics)
|
|
#expect(props?["SUMMARY"] == "This is a very long summary that was folded")
|
|
}
|
|
|
|
@Test("strips parameters from property names")
|
|
func stripsParameters() {
|
|
let ics = "BEGIN:VTODO\r\nDTSTART;VALUE=DATE:20260315\r\nDUE;VALUE=DATE:20260320\r\nEND:VTODO"
|
|
let props = VTODOParser.parse(ics)
|
|
#expect(props?["DTSTART"] == "20260315")
|
|
#expect(props?["DUE"] == "20260320")
|
|
}
|
|
|
|
// MARK: - Date parsing
|
|
|
|
@Test("parses DATE-TIME with Z")
|
|
func parseDateTimeUTC() {
|
|
let date = VTODOParser.parseDate("20260315T090000Z")
|
|
#expect(date != nil)
|
|
|
|
let calendar = Calendar(identifier: .gregorian)
|
|
let components = calendar.dateComponents(in: TimeZone(identifier: "UTC")!, from: date!)
|
|
#expect(components.year == 2026)
|
|
#expect(components.month == 3)
|
|
#expect(components.day == 15)
|
|
#expect(components.hour == 9)
|
|
#expect(components.minute == 0)
|
|
}
|
|
|
|
@Test("parses DATE-TIME without Z")
|
|
func parseDateTimeLocal() {
|
|
let date = VTODOParser.parseDate("20260315T090000")
|
|
#expect(date != nil)
|
|
}
|
|
|
|
@Test("parses DATE only")
|
|
func parseDateOnly() {
|
|
let date = VTODOParser.parseDate("20260315")
|
|
#expect(date != nil)
|
|
|
|
let calendar = Calendar(identifier: .gregorian)
|
|
let components = calendar.dateComponents(in: TimeZone(identifier: "UTC")!, from: date!)
|
|
#expect(components.year == 2026)
|
|
#expect(components.month == 3)
|
|
#expect(components.day == 15)
|
|
}
|
|
|
|
@Test("returns nil for invalid date")
|
|
func invalidDate() {
|
|
#expect(VTODOParser.parseDate("not-a-date") == nil)
|
|
#expect(VTODOParser.parseDate("") == nil)
|
|
}
|
|
|
|
// MARK: - Date round-trip
|
|
|
|
@Test("date round-trip preserves value")
|
|
func dateRoundTrip() {
|
|
let original = Date(timeIntervalSince1970: 1_773_763_200) // 2026-03-15T12:00:00Z
|
|
let formatted = VTODOParser.formatDate(original)
|
|
let parsed = VTODOParser.parseDate(formatted)
|
|
#expect(parsed != nil)
|
|
#expect(abs(parsed!.timeIntervalSince(original)) < 1)
|
|
}
|
|
|
|
@Test("date-only round-trip preserves value")
|
|
func dateOnlyRoundTrip() {
|
|
let original = VTODOParser.parseDate("20260315")!
|
|
let formatted = VTODOParser.formatDateOnly(original)
|
|
#expect(formatted == "20260315")
|
|
}
|
|
|
|
// MARK: - Unescape round-trip
|
|
|
|
@Test("unescape handles all special characters")
|
|
func unescapeSpecialChars() {
|
|
#expect(VTODOParser.unescapeText("hello\\nworld") == "hello\nworld")
|
|
#expect(VTODOParser.unescapeText("a\\,b") == "a,b")
|
|
#expect(VTODOParser.unescapeText("a\\;b") == "a;b")
|
|
#expect(VTODOParser.unescapeText("a\\\\b") == "a\\b")
|
|
#expect(VTODOParser.unescapeText("plain text") == "plain text")
|
|
}
|
|
|
|
@Test("unescape handles uppercase N for newline")
|
|
func unescapeUppercaseN() {
|
|
#expect(VTODOParser.unescapeText("line1\\Nline2") == "line1\nline2")
|
|
}
|
|
}
|