Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MeetingBar/Core/Managers/EventManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public class EventManager: ObservableObject {

// E) When any fires, fetch calendars & events
trigger
.flatMap { [weak self] _ -> AnyPublisher<([MBCalendar], [MBEvent]), Never> in
.flatMap(maxPublishers: .max(1)) { [weak self] _ -> AnyPublisher<([MBCalendar], [MBEvent]), Never> in
guard let self = self else {
return Just(([], [])).eraseToAnyPublisher()
}
Expand Down
69 changes: 69 additions & 0 deletions MeetingBarTests/EventManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,72 @@ final class RefreshTriggerTests: BaseTestCase {
await fulfillment(of: [exp], timeout: 1)
}
}

@MainActor
final class RefreshErrorHandlingTests: BaseTestCase {

private var cancellables = Set<AnyCancellable>()

func test_eventsPreservedOnRefreshFailure() async throws {
// Arrange: set up fake store with initial data
let initialCal = MBCalendar(title: "Initial Cal", id: "cal1", source: nil, email: nil, color: .black)
let initialEvent = makeFakeEvent(
id: "E1",
start: Date().addingTimeInterval(60),
end: Date().addingTimeInterval(3600)
)
let store = FakeEventStore(calendars: [initialCal], events: [initialEvent])
let manager = EventManager(provider: store, refreshInterval: 0)

// Wait for initial data to be published
let initialExp = expectation(description: "initial data")
manager.$events
.drop(while: \.isEmpty)
.first()
.sink { events in
XCTAssertEqual(events, [initialEvent])
initialExp.fulfill()
}
.store(in: &cancellables)

await fulfillment(of: [initialExp], timeout: 1.0)

// Verify initial calendars were also published
XCTAssertEqual(manager.calendars, [initialCal])

// Record the initial call counts
let initialCalendarCallCount = store.fetchCalendarsCallCount
let initialEventsCallCount = store.fetchEventsCallCount

// Act: configure store to throw errors on next fetch
store.shouldThrowOnFetchCalendars = true
store.shouldThrowOnFetchEvents = true

// Set up expectation that published values won't change (no new publications expected)
// We use a negated expectation since we DON'T want the values to change
var receivedUnexpectedUpdate = false
let noChangeSubscription = manager.$calendars
.dropFirst() // skip current value
.sink { _ in
receivedUnexpectedUpdate = true
}

// Trigger refresh that will fail
try await manager.refreshSources()

// Wait for async operations to complete
try await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds

// Assert: verify fetch was attempted (call count increased)
XCTAssertGreaterThan(store.fetchCalendarsCallCount, initialCalendarCallCount, "Fetch should have been attempted")

// Assert: calendars and events should still contain initial data (not empty)
XCTAssertEqual(manager.calendars, [initialCal], "Calendars should be preserved on fetch failure")
XCTAssertEqual(manager.events, [initialEvent], "Events should be preserved on fetch failure")

// Verify no unexpected updates occurred
XCTAssertFalse(receivedUnexpectedUpdate, "Published values should not have changed since they match the fallback values")

noChangeSubscription.cancel()
}
}
16 changes: 14 additions & 2 deletions MeetingBarTests/Helpers/FakeEventStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import Foundation
final class FakeEventStore: EventStore {
var stubbedCalendars: [MBCalendar]
var stubbedEvents: [MBEvent]
var shouldThrowOnFetchCalendars: Bool = false
var shouldThrowOnFetchEvents: Bool = false
var fetchCalendarsCallCount: Int = 0
var fetchEventsCallCount: Int = 0

init(calendars: [MBCalendar] = [], events: [MBEvent] = []) {
stubbedCalendars = calendars
Expand All @@ -21,15 +25,23 @@ final class FakeEventStore: EventStore {
// MARK: - EventStore

func fetchAllCalendars() async throws -> [MBCalendar] {
stubbedCalendars
fetchCalendarsCallCount += 1
if shouldThrowOnFetchCalendars {
throw EventManagerError.eventStoreNotAvailable
}
return stubbedCalendars
}

func fetchEventsForDateRange(
for _: [MBCalendar],
from _: Date,
to _: Date
) async throws -> [MBEvent] {
stubbedEvents
fetchEventsCallCount += 1
if shouldThrowOnFetchEvents {
throw EventManagerError.eventStoreNotAvailable
}
return stubbedEvents
}

func refreshSources() async { /* no-op */ }
Expand Down
Loading