Files
MagnumOpus/Packages/MagnumOpusCore/Tests/TaskStoreTests/VTODOParserTests.swift
2026-03-14 10:31:00 +01:00

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")
}
}