import XCTest @testable import honeyDue /// Tests for the per-residence widget filter added in gitea#6. /// /// `WidgetDataManager.filterTasks(_:forResidenceId:)` is the pure /// function the widget timeline provider calls when a configuration /// intent has a residence selected. These tests guarantee the contract /// stays stable: nil → pass-through, matching id → only matching tasks, /// no match → empty, missing residenceId on a task → never leaks into /// a residence-scoped widget. final class WidgetResidenceFilterTests: XCTestCase { private func makeTask( id: Int, residenceId: Int? = nil ) -> WidgetDataManager.WidgetTask { WidgetDataManager.WidgetTask( id: id, title: "Task \(id)", description: nil, priority: nil, inProgress: false, dueDate: nil, category: nil, residenceId: residenceId, residenceName: nil, isOverdue: false, isDueWithin7Days: false, isDue8To30Days: false ) } func testNilResidenceReturnsAllTasks() { // "All residences" config — widget passes nil, gets every task. let tasks = [ makeTask(id: 1, residenceId: 10), makeTask(id: 2, residenceId: 20), makeTask(id: 3, residenceId: nil), ] let result = WidgetDataManager.filterTasks(tasks, forResidenceId: nil) XCTAssertEqual(result.map(\.id), [1, 2, 3]) } func testMatchingResidenceKeepsOnlyMatchingTasks() { let tasks = [ makeTask(id: 1, residenceId: 10), makeTask(id: 2, residenceId: 20), makeTask(id: 3, residenceId: 10), makeTask(id: 4, residenceId: 30), ] let result = WidgetDataManager.filterTasks(tasks, forResidenceId: 10) XCTAssertEqual(result.map(\.id), [1, 3]) } func testUnknownResidenceReturnsEmpty() { let tasks = [ makeTask(id: 1, residenceId: 10), makeTask(id: 2, residenceId: 20), ] let result = WidgetDataManager.filterTasks(tasks, forResidenceId: 999) XCTAssertTrue(result.isEmpty) } func testNilResidenceIdOnTaskDoesNotMatchScopedConfiguration() { // A task written by an older app build (no `residence_id` in JSON) // must NOT leak into a residence-scoped widget — we'd rather hide // it than misattribute it to the wrong home. let tasks = [ makeTask(id: 1, residenceId: 10), makeTask(id: 2, residenceId: nil), ] let result = WidgetDataManager.filterTasks(tasks, forResidenceId: 10) XCTAssertEqual(result.map(\.id), [1]) } func testFilterPreservesInputOrder() { // The filter is a pure subset op — no sorting side effects. // Timeline provider relies on this so its sort step (overdue // first, then by due date) operates on already-filtered tasks. let tasks = [ makeTask(id: 5, residenceId: 1), makeTask(id: 3, residenceId: 1), makeTask(id: 7, residenceId: 1), makeTask(id: 1, residenceId: 2), ] let result = WidgetDataManager.filterTasks(tasks, forResidenceId: 1) XCTAssertEqual(result.map(\.id), [5, 3, 7]) } }