Files
reyna-cli/native/ReynaCLIHost/Tests/ReynaCLIHostTests/CalendarEventsTests.swift
T
Adolfo Reyna 9fd04b0ce4 feat(privacy-host): add signed native calendar contacts and reminders host
Add the owner-only AF_UNIX Reyna CLI privacy host, strict signed-app installation, and typed native routing for Calendar, Contacts, and Reminders.\n\nAdd bounded system-status paths and config-only direct local-service wrappers. Preserve MacMiniMCP pending explicit cutover approval.\n\nApple Notes is intentionally deferred: no native Notes operations, Apple Events declaration, or Automation helper are included; legacy Notes handling remains untouched.
2026-08-03 20:27:54 -04:00

420 lines
24 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import XCTest
@testable import ReynaCLIHostCore
import Foundation
// TDD for calendar.events.list and calendar.event.create – fake providers only, no live EventKit
final class CalendarEventsTests: XCTestCase {
// MARK: - Helper types
struct FakeCalendarListForSelection: CalendarListProviding {
var calendars: [CalendarListItem]
func listCalendars() throws -> [CalendarListItem] { calendars }
}
struct FakeEventsProvider: CalendarEventsListProviding {
var events: [CalendarEventItem]
var shouldThrow: CalendarProviderError? = nil
func listEvents(start: Date, end: Date, calendarId: String?, calendarTitle: String?, limit: Int) throws -> [CalendarEventItem] {
if let err = shouldThrow { throw err }
var filtered = events
if let cid = calendarId {
filtered = filtered.filter { $0.calendar_id == cid }
} else if let ctitle = calendarTitle {
filtered = filtered.filter { $0.calendar_title == ctitle }
}
return filtered
}
}
struct SelectingEventsProvider: CalendarEventsListProviding {
var availableCalendars: [CalendarListItem]
var events: [CalendarEventItem]
func listEvents(start: Date, end: Date, calendarId: String?, calendarTitle: String?, limit: Int) throws -> [CalendarEventItem] {
if let cid = calendarId {
guard availableCalendars.contains(where: { $0.id == cid }) else {
throw CalendarProviderError.invalidRequest("Calendar not found: \(cid)")
}
return events.filter { $0.calendar_id == cid }.prefix(limit).map { $0 }
}
if let title = calendarTitle {
let matched = availableCalendars.filter { $0.title == title }
if matched.isEmpty {
throw CalendarProviderError.invalidRequest("Calendar not found: \(title)")
}
if matched.count > 1 {
throw CalendarProviderError.invalidRequest("Ambiguous calendar title: \(title) matches \(matched.count) calendars")
}
return events.filter { $0.calendar_title == title }.prefix(limit).map { $0 }
}
let sorted = events.sorted { $0.start < $1.start }
return Array(sorted.prefix(limit))
}
}
struct FakeCreateProvider: CalendarEventCreateProviding {
var shouldThrow: CalendarProviderError? = nil
var willReturn: CalendarEventItem
func createEvent(title: String, start: Date, end: Date, allDay: Bool, notes: String?, location: String?, calendarId: String?, calendarTitle: String?) throws -> CalendarEventItem {
if let err = shouldThrow { throw err }
return willReturn
}
}
struct SelectingCreateProvider: CalendarEventCreateProviding {
var availableCalendars: [CalendarListItem]
var writableIds: Set<String>
var willReturn: CalendarEventItem
func createEvent(title: String, start: Date, end: Date, allDay: Bool, notes: String?, location: String?, calendarId: String?, calendarTitle: String?) throws -> CalendarEventItem {
if let cid = calendarId {
guard let cal = availableCalendars.first(where: { $0.id == cid }) else {
throw CalendarProviderError.invalidRequest("Calendar not found: \(cid)")
}
guard writableIds.contains(cal.id) else {
throw CalendarProviderError.invalidRequest("Calendar is read-only: \(cal.title)")
}
return willReturn
}
if let t = calendarTitle {
let matched = availableCalendars.filter { $0.title == t }
if matched.isEmpty { throw CalendarProviderError.invalidRequest("Calendar not found: \(t)") }
if matched.count > 1 { throw CalendarProviderError.invalidRequest("Ambiguous calendar title: \(t) matches \(matched.count) calendars") }
guard writableIds.contains(matched[0].id) else {
throw CalendarProviderError.invalidRequest("Calendar is read-only: \(matched[0].title)")
}
return willReturn
}
throw CalendarProviderError.invalidRequest("Calendar must be specified by id or exact unique title")
}
}
struct DeniedEventsProvider: CalendarEventsListProviding {
func listEvents(start: Date, end: Date, calendarId: String?, calendarTitle: String?, limit: Int) throws -> [CalendarEventItem] {
throw CalendarProviderError.permissionRequired
}
}
struct FailEventsProvider: CalendarEventsListProviding {
func listEvents(start: Date, end: Date, calendarId: String?, calendarTitle: String?, limit: Int) throws -> [CalendarEventItem] {
throw CalendarProviderError.unavailable("disk fail")
}
}
struct DeniedCreateProvider: CalendarEventCreateProviding {
func createEvent(title: String, start: Date, end: Date, allDay: Bool, notes: String?, location: String?, calendarId: String?, calendarTitle: String?) throws -> CalendarEventItem {
throw CalendarProviderError.permissionRequired
}
}
struct MockAuthProvider: CalendarAuthorizationProviding {
var status: CalendarAuthorizationStatus
func authorizationStatus() -> CalendarAuthorizationStatus { status }
func requestFullAccess() throws -> Bool { false }
}
final class CountingCreate: CalendarEventCreateProviding, @unchecked Sendable {
var count = 0
func createEvent(title: String, start: Date, end: Date, allDay: Bool, notes: String?, location: String?, calendarId: String?, calendarTitle: String?) throws -> CalendarEventItem {
count += 1
return CalendarEventItem(id: "sample", title: "Sample", start: "2026-01-01T10:00:00Z", end: "2026-01-01T11:00:00Z", all_day: false, calendar_id: "cal1", calendar_title: "Home", notes: nil, location: nil)
}
}
func sampleEvent() -> CalendarEventItem {
CalendarEventItem(id: "sample", title: "Sample", start: "2026-01-01T10:00:00Z", end: "2026-01-01T11:00:00Z", all_day: false, calendar_id: "cal1", calendar_title: "Home", notes: nil, location: nil)
}
// MARK: - List tests
func testEventsListMissingStartReturnsInvalidRequest() {
let req = Request(id: "e1", operation: "calendar.events.list", arguments: .object(["end": .string("2026-01-02T00:00:00Z")]))
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "invalid_request")
}
func testEventsListInvalidISODate() {
let args: JSONValue = .object(["start": .string("not-a-date"), "end": .string("2026-01-02T00:00:00Z")])
let req = Request(id: "e2", operation: "calendar.events.list", arguments: args)
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "invalid_request")
}
func testEventsListStartAfterEnd() {
let args: JSONValue = .object(["start": .string("2026-01-03T00:00:00Z"), "end": .string("2026-01-02T00:00:00Z")])
let req = Request(id: "e3", operation: "calendar.events.list", arguments: args)
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "invalid_request")
}
func testEventsListLimitBounded() {
let argsLow: JSONValue = .object(["start": .string("2026-01-01T00:00:00Z"), "end": .string("2026-01-02T00:00:00Z"), "limit": .number(0)])
let reqLow = Request(id: "e4", operation: "calendar.events.list", arguments: argsLow)
let respLow = dispatch(request: reqLow, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(respLow.ok)
XCTAssertEqual(respLow.error?.code, "invalid_request")
let argsHigh: JSONValue = .object(["start": .string("2026-01-01T00:00:00Z"), "end": .string("2026-01-02T00:00:00Z"), "limit": .number(251)])
let reqHigh = Request(id: "e5", operation: "calendar.events.list", arguments: argsHigh)
let respHigh = dispatch(request: reqHigh, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(respHigh.ok)
XCTAssertEqual(respHigh.error?.code, "invalid_request")
}
func testEventsListSuccessSortedAndMinimalFields() {
let ev1 = CalendarEventItem(id: "2", title: "B", start: "2026-01-01T11:00:00Z", end: "2026-01-01T12:00:00Z", all_day: false, calendar_id: "cal1", calendar_title: "Home", notes: "n2", location: "loc2")
let ev2 = CalendarEventItem(id: "1", title: "A", start: "2026-01-01T10:00:00Z", end: "2026-01-01T11:00:00Z", all_day: false, calendar_id: "cal1", calendar_title: "Home", notes: nil, location: nil)
let provider = FakeEventsProvider(events: [ev1, ev2])
let args: JSONValue = .object(["start": .string("2026-01-01T00:00:00Z"), "end": .string("2026-01-02T00:00:00Z"), "limit": .number(10)])
let req = Request(id: "e6", operation: "calendar.events.list", arguments: args)
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: provider, createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized))
XCTAssertTrue(resp.ok)
XCTAssertEqual(resp.result?.events?.count, 2)
XCTAssertEqual(resp.result?.events?.first?.id, "1")
let encoded = try! JSONEncoder().encode(resp)
let obj = try! JSONSerialization.jsonObject(with: encoded) as! [String: Any]
let result = obj["result"] as! [String: Any]
let events = result["events"] as! [[String: Any]]
for ev in events {
XCTAssertNotNil(ev["id"])
XCTAssertNotNil(ev["title"])
XCTAssertNotNil(ev["start"])
XCTAssertNotNil(ev["end"])
XCTAssertNotNil(ev["calendar_id"])
XCTAssertNotNil(ev["calendar_title"])
let allowed = Set(["id","title","start","end","all_day","calendar_id","calendar_title","notes","location"])
XCTAssertTrue(Set(ev.keys).isSubset(of: allowed), "Unexpected keys: \(ev.keys)")
}
}
func testEventsListCalendarIdWinsOverTitle() {
let calendars = [
CalendarListItem(id: "id1", title: "Home", source: "iCloud", type: "caldav"),
CalendarListItem(id: "id2", title: "Home", source: "Local", type: "local")
]
let ev1 = CalendarEventItem(id: "e1", title: "T", start: "2026-01-01T10:00:00Z", end: "2026-01-01T11:00:00Z", all_day: false, calendar_id: "id1", calendar_title: "Home", notes: nil, location: nil)
let ev2 = CalendarEventItem(id: "e2", title: "T", start: "2026-01-01T11:00:00Z", end: "2026-01-01T12:00:00Z", all_day: false, calendar_id: "id2", calendar_title: "Home", notes: nil, location: nil)
let provider = SelectingEventsProvider(availableCalendars: calendars, events: [ev1, ev2])
let args: JSONValue = .object([
"start": .string("2026-01-01T00:00:00Z"),
"end": .string("2026-01-02T00:00:00Z"),
"calendar_id": .string("id1"),
"calendar": .string("Home"),
"limit": .number(10)
])
let req = Request(id: "e7", operation: "calendar.events.list", arguments: args)
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: calendars), eventsProvider: provider, createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized))
XCTAssertTrue(resp.ok)
XCTAssertEqual(resp.result?.events?.count, 1)
XCTAssertEqual(resp.result?.events?.first?.calendar_id, "id1")
}
func testEventsListUnknownCalendarFailsDeterministic() {
let calendars = [CalendarListItem(id: "id1", title: "Home", source: "iCloud", type: "caldav")]
let provider = SelectingEventsProvider(availableCalendars: calendars, events: [])
let args: JSONValue = .object([
"start": .string("2026-01-01T00:00:00Z"),
"end": .string("2026-01-02T00:00:00Z"),
"calendar": .string("Work")
])
let req = Request(id: "e8", operation: "calendar.events.list", arguments: args)
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: calendars), eventsProvider: provider, createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "invalid_request")
}
func testEventsListAmbiguousTitleFails() {
let calendars = [
CalendarListItem(id: "id1", title: "Home", source: "iCloud", type: "caldav"),
CalendarListItem(id: "id2", title: "Home", source: "Local", type: "local")
]
let provider = SelectingEventsProvider(availableCalendars: calendars, events: [])
let args: JSONValue = .object([
"start": .string("2026-01-01T00:00:00Z"),
"end": .string("2026-01-02T00:00:00Z"),
"calendar": .string("Home")
])
let req = Request(id: "e9", operation: "calendar.events.list", arguments: args)
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: calendars), eventsProvider: provider, createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "invalid_request")
XCTAssertTrue(resp.error?.message.contains("Ambiguous") ?? false)
}
func testEventsListPermissionRequired() {
let args: JSONValue = .object([
"start": .string("2026-01-01T00:00:00Z"),
"end": .string("2026-01-02T00:00:00Z")
])
let req = Request(id: "e10", operation: "calendar.events.list", arguments: args)
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: DeniedEventsProvider(), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "permission_required")
}
func testEventsListProviderFailureMapsToUnavailable() {
let args: JSONValue = .object([
"start": .string("2026-01-01T00:00:00Z"),
"end": .string("2026-01-02T00:00:00Z")
])
let req = Request(id: "e11", operation: "calendar.events.list", arguments: args)
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FailEventsProvider(), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "calendar_unavailable")
}
// MARK: - Create tests
func testCreateMissingTitleInvalid() {
let args: JSONValue = .object([
"start": .string("2026-01-01T10:00:00Z"),
"end": .string("2026-01-01T11:00:00Z"),
"calendar_id": .string("id1")
])
let req = Request(id: "c1", operation: "calendar.event.create", arguments: args)
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "invalid_request")
}
func testCreateStartAfterEndInvalid() {
let args: JSONValue = .object([
"title": .string("Meeting"),
"start": .string("2026-01-01T12:00:00Z"),
"end": .string("2026-01-01T11:00:00Z"),
"calendar_id": .string("id1")
])
let req = Request(id: "c2", operation: "calendar.event.create", arguments: args)
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "invalid_request")
}
func testCreateInvalidISO() {
let args: JSONValue = .object([
"title": .string("Meeting"),
"start": .string("bad-date"),
"end": .string("2026-01-01T11:00:00Z"),
"calendar_id": .string("id1")
])
let req = Request(id: "c3", operation: "calendar.event.create", arguments: args)
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "invalid_request")
}
func testCreateNoCalendarSpecifiedFailsNoDefault() {
let args: JSONValue = .object([
"title": .string("Meeting"),
"start": .string("2026-01-01T10:00:00Z"),
"end": .string("2026-01-01T11:00:00Z")
])
let req = Request(id: "c4", operation: "calendar.event.create", arguments: args)
let calendars = [CalendarListItem(id: "id1", title: "Home", source: "iCloud", type: "caldav")]
let provider = SelectingCreateProvider(availableCalendars: calendars, writableIds: ["id1"], willReturn: sampleEvent())
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: calendars), eventsProvider: FakeEventsProvider(events: []), createProvider: provider, authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "invalid_request")
XCTAssertTrue(resp.error?.message.contains("must be specified") ?? false)
}
func testCreateUnknownCalendarFails() {
let args: JSONValue = .object([
"title": .string("Meeting"),
"start": .string("2026-01-01T10:00:00Z"),
"end": .string("2026-01-01T11:00:00Z"),
"calendar": .string("Nonexistent")
])
let req = Request(id: "c5", operation: "calendar.event.create", arguments: args)
let calendars = [CalendarListItem(id: "id1", title: "Home", source: "iCloud", type: "caldav")]
let provider = SelectingCreateProvider(availableCalendars: calendars, writableIds: ["id1"], willReturn: sampleEvent())
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: calendars), eventsProvider: FakeEventsProvider(events: []), createProvider: provider, authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "invalid_request")
}
func testCreateAmbiguousTitleFails() {
let args: JSONValue = .object([
"title": .string("Meeting"),
"start": .string("2026-01-01T10:00:00Z"),
"end": .string("2026-01-01T11:00:00Z"),
"calendar": .string("Home")
])
let req = Request(id: "c6", operation: "calendar.event.create", arguments: args)
let calendars = [
CalendarListItem(id: "id1", title: "Home", source: "iCloud", type: "caldav"),
CalendarListItem(id: "id2", title: "Home", source: "Local", type: "local")
]
let provider = SelectingCreateProvider(availableCalendars: calendars, writableIds: ["id1","id2"], willReturn: sampleEvent())
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: calendars), eventsProvider: FakeEventsProvider(events: []), createProvider: provider, authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "invalid_request")
}
func testCreateReadOnlyCalendarFails() {
let args: JSONValue = .object([
"title": .string("Meeting"),
"start": .string("2026-01-01T10:00:00Z"),
"end": .string("2026-01-01T11:00:00Z"),
"calendar_id": .string("id1")
])
let req = Request(id: "c7", operation: "calendar.event.create", arguments: args)
let calendars = [CalendarListItem(id: "id1", title: "Birthdays", source: "iCloud", type: "birthday")]
let provider = SelectingCreateProvider(availableCalendars: calendars, writableIds: [], willReturn: sampleEvent())
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: calendars), eventsProvider: FakeEventsProvider(events: []), createProvider: provider, authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "invalid_request")
}
func testCreateSuccessReturnsMetadata() {
let args: JSONValue = .object([
"title": .string("Meeting"),
"start": .string("2026-01-01T10:00:00Z"),
"end": .string("2026-01-01T11:00:00Z"),
"calendar_id": .string("id1"),
"notes": .string("bring docs"),
"location": .string("Room 1")
])
let req = Request(id: "c8", operation: "calendar.event.create", arguments: args)
let created = CalendarEventItem(id: "new-id", title: "Meeting", start: "2026-01-01T10:00:00Z", end: "2026-01-01T11:00:00Z", all_day: false, calendar_id: "id1", calendar_title: "Home", notes: "bring docs", location: "Room 1")
let provider = FakeCreateProvider(willReturn: created)
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: provider, authProvider: MockAuthProvider(status: .authorized))
XCTAssertTrue(resp.ok)
XCTAssertEqual(resp.result?.event?.id, "new-id")
XCTAssertEqual(resp.result?.event?.calendar_id, "id1")
XCTAssertEqual(resp.result?.operation, "calendar.event.create")
}
func testCreatePermissionRequired() {
let args: JSONValue = .object([
"title": .string("Meeting"),
"start": .string("2026-01-01T10:00:00Z"),
"end": .string("2026-01-01T11:00:00Z"),
"calendar_id": .string("id1")
])
let req = Request(id: "c9", operation: "calendar.event.create", arguments: args)
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: DeniedCreateProvider(), authProvider: MockAuthProvider(status: .authorized))
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "permission_required")
}
func testListDoesNotTriggerCreate() {
let args: JSONValue = .object([
"start": .string("2026-01-01T00:00:00Z"),
"end": .string("2026-01-02T00:00:00Z")
])
let req = Request(id: "iso", operation: "calendar.events.list", arguments: args)
let counter = CountingCreate()
let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: counter, authProvider: MockAuthProvider(status: .authorized))
XCTAssertTrue(resp.ok)
XCTAssertEqual(counter.count, 0, "list must not trigger create")
}
}