oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1import XCTest
2import RastrilloApple
3@testable import CalendarKit
4
5// B11 (polish-audit PLAN): a failed load used to run `events = []`, so
6// any blip — a dropped connection, a 500, going offline — blanked a
7// calendar that was on screen a second earlier, and the menu-bar panel
8// shares the model, so opening it offline blanked the window too.
9final class LoadFailureTests: XCTestCase {
10 @MainActor func testFailedReloadKeepsLastGoodEventsAndReportsTheError() async throws {
11 let process = Process()
12 process.executableURL = URL(fileURLWithPath: "/usr/bin/python3")
13 process.arguments = ["-u", "-c", """
14import http.server, json
15failing=False
16event={'id':7,'calendar_id':1,'status':'confirmed','title':'Standup','location':'','notes':'','all_day':False,
17 'start_local':'2030-10-25T09:00','end_local':'2030-10-25T09:30','tz':'Europe/Dublin','start':1919232000,'end':1919233800,
18 'recurring':False,'writable':True,'calendar_name':'Personal','occurrence':'','day_start':'2030-10-25','day_end':'2030-10-25'}
19class Handler(http.server.BaseHTTPRequestHandler):
20 def log_message(self,*args): pass
21 def reply(self,code,value):
22 data=json.dumps(value).encode(); self.send_response(code); self.send_header('Content-Type','application/json'); self.send_header('Content-Length',str(len(data))); self.end_headers(); self.wfile.write(data)
23 def do_GET(self):
24 global failing
25 if self.path=='/fail': failing=True; self.reply(200,{}); return
26 if self.path=='/recover': failing=False; self.reply(200,{}); return
27 if failing: self.reply(500,{'error':'fixture outage'}); return
28 if self.path.startswith('/api/native/session'):
29 self.reply(200,{'week_start':1,'zone':'Europe/Dublin','calendars':[{'id':1,'name':'Personal','hue':200,'writable':True}],'default_calendar_id':1}); return
30 if self.path.startswith('/api/native/events'): self.reply(200,{'events':[event]}); return
31 self.reply(404,{})
32server=http.server.ThreadingHTTPServer(('127.0.0.1',0),Handler)
33print(server.server_port,flush=True); server.serve_forever()
34"""]
35 let output = Pipe(); process.standardOutput = output; process.standardError = FileHandle.nullDevice
36 try process.run()
37 defer { process.terminate() }
38 let data = output.fileHandleForReading.availableData
39 let port = try XCTUnwrap(String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines))
40 let client = NativeHTTP(origin: URL(string: "http://127.0.0.1:\(port)")!)
41 let model = CalendarModel(zone: "Europe/Dublin", date: Date(timeIntervalSince1970: 1919232000))
42
43 // A first load that FAILS has nothing to keep: the error stands alone.
44 _ = try await client.response("/fail")
45 await model.load(client)
46 XCTAssertFalse(model.error.isEmpty)
47 XCTAssertFalse(model.hasLoaded)
48 XCTAssertTrue(model.events.isEmpty)
49
50 // It recovers, and the day shows its event.
51 _ = try await client.response("/recover")
52 await model.load(client)
53 XCTAssertTrue(model.error.isEmpty)
54 XCTAssertTrue(model.hasLoaded)
55 XCTAssertEqual(model.events.map(\.title), ["Standup"])
56 XCTAssertEqual(model.calendars.map(\.name), ["Personal"])
57
58 // Then the server goes away. The refresh fails, the error is
59 // reported, and what was on screen stays on screen.
60 _ = try await client.response("/fail")
61 await model.load(client)
62 XCTAssertFalse(model.error.isEmpty, "a failed refresh must still say it failed")
63 XCTAssertTrue(model.hasLoaded)
64 XCTAssertFalse(model.loading)
65 XCTAssertEqual(model.events.map(\.title), ["Standup"], "a failed refresh blanked the events on screen")
66 XCTAssertEqual(model.calendars.map(\.name), ["Personal"])
67 XCTAssertEqual(model.dayEvents(model.date).map(\.title), ["Standup"])
68
69 // Signing out is the one thing that does clear it.
70 model.clear()
71 XCTAssertFalse(model.hasLoaded)
72 XCTAssertTrue(model.events.isEmpty)
73 }
74}
75