| 1 | import XCTest |
| 2 | import RastrilloApple |
| 3 | @testable import CalendarKit |
| 4 | |
| 5 | final class SessionLifecycleTests: XCTestCase { |
| 6 | // Independent reviewer mutation, 2026-09-13: remove saveQuick's guard after |
| 7 | // the POST response. Four assertions fail (cancellation/date/notice/result); |
| 8 | // restored source passes all five Swift tests. |
| 9 | @MainActor func testLateSavesCannotReloadClearedSession() async throws { |
| 10 | for quick in [false, true] { |
| 11 | let process = Process() |
| 12 | process.executableURL = URL(fileURLWithPath: "/usr/bin/python3") |
| 13 | process.arguments = ["-u", "-c", """ |
| 14 | import http.server, threading, json |
| 15 | started=threading.Event(); release=threading.Event(); reads=0 |
| 16 | class Handler(http.server.BaseHTTPRequestHandler): |
| 17 | def log_message(self,*args): pass |
| 18 | def reply(self,value): |
| 19 | data=json.dumps(value).encode(); self.send_response(200); self.send_header('Content-Length',str(len(data))); self.end_headers(); self.wfile.write(data) |
| 20 | def do_POST(self): |
| 21 | self.rfile.read(int(self.headers.get('Content-Length','0'))); started.set(); release.wait(10); self.reply({'id':1,'warning':'Event saved. Reminder unavailable.'}) |
| 22 | def do_GET(self): |
| 23 | global reads |
| 24 | if self.path=='/wait': started.wait(10); self.reply({}); return |
| 25 | if self.path=='/release': release.set(); self.reply({}); return |
| 26 | if self.path=='/counts': self.reply({'reads':reads}); return |
| 27 | reads+=1; self.reply({}) |
| 28 | server=http.server.ThreadingHTTPServer(('127.0.0.1',0),Handler) |
| 29 | print(server.server_port,flush=True); server.serve_forever() |
| 30 | """] |
| 31 | let output = Pipe(); process.standardOutput = output; process.standardError = FileHandle.nullDevice |
| 32 | try process.run() |
| 33 | defer { process.terminate() } |
| 34 | let data = output.fileHandleForReading.availableData |
| 35 | let port = try XCTUnwrap(String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)) |
| 36 | let client = NativeHTTP(origin: URL(string: "http://127.0.0.1:\(port)")!) |
| 37 | let model = CalendarModel(zone: "Europe/Dublin") |
| 38 | let draft = QuickDraft(calendar_id: 1, title: "Fixture", location: "", start_local: "2030-10-25T09:00", end_local: "2030-10-25T10:00", tz: "Europe/Dublin", all_day: false, repeat: "none", attendees: []) |
| 39 | let preview = QuickPreview(draft: draft, when: "Fixture time", repeat_label: "", calendar_name: "Personal", assumptions: []) |
| 40 | let task = Task { |
| 41 | if quick { try await model.saveQuick(preview, client: client) } |
| 42 | else { try await model.save(CalendarEvent(), client: client) } |
| 43 | } |
| 44 | _ = try await client.response("/wait") |
| 45 | model.clear() |
| 46 | let clearedDate = model.date |
| 47 | _ = try await client.response("/release") |
| 48 | do { try await task.value; XCTFail("Old save completed against cleared session") } |
| 49 | catch is CancellationError { } |
| 50 | await model.load(client) // Even queued refresh callbacks cannot revive this model. |
| 51 | struct Counts: Decodable { let reads: Int } |
| 52 | let counts: Counts = try await client.request("/counts") |
| 53 | XCTAssertEqual(counts.reads, 0) |
| 54 | XCTAssertEqual(model.date, clearedDate) |
| 55 | XCTAssertTrue(model.events.isEmpty) |
| 56 | XCTAssertTrue(model.calendars.isEmpty) |
| 57 | XCTAssertTrue(model.notice.isEmpty) |
| 58 | XCTAssertTrue(model.quickSavedNotice.isEmpty) |
| 59 | XCTAssertFalse(model.quickSaving) |
| 60 | } |
| 61 | } |
| 62 | @MainActor func testSystemTimeZoneDefaultAndOverride() { |
| 63 | let key = "calendar.timeZoneOverride" |
| 64 | let prior = UserDefaults.standard.object(forKey: key) |
| 65 | defer { if let prior { UserDefaults.standard.set(prior, forKey: key) } else { UserDefaults.standard.removeObject(forKey: key) } } |
| 66 | UserDefaults.standard.removeObject(forKey: key) |
| 67 | XCTAssertEqual(CalendarModel().zone, TimeZone.current.identifier) |
| 68 | UserDefaults.standard.set("Pacific/Kiritimati", forKey: key) |
| 69 | XCTAssertEqual(CalendarModel().newEvent().tz, "Pacific/Kiritimati") |
| 70 | UserDefaults.standard.set("", forKey: key) |
| 71 | XCTAssertEqual(CalendarModel().zone, TimeZone.current.identifier) |
| 72 | } |
| 73 | } |
| 74 | |