SavedJobsPersistenceTests.swift 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. //
  2. // SavedJobsPersistenceTests.swift
  3. // indeed_ai_job_searchTests
  4. //
  5. import XCTest
  6. @testable import indeed_ai_job_search
  7. @MainActor
  8. final class SwiftDataSavedJobsStoreTests: XCTestCase {
  9. private var controller: PersistenceController!
  10. private var store: SwiftDataSavedJobsStore!
  11. override func setUp() {
  12. super.setUp()
  13. controller = PersistenceController.makeInMemory()
  14. store = SwiftDataSavedJobsStore(controller: controller)
  15. }
  16. override func tearDown() {
  17. store = nil
  18. controller = nil
  19. super.tearDown()
  20. }
  21. func testSaveUnsaveSurvivesReload() {
  22. let listing = makeListing(key: "abc123def4567890", title: "iOS Developer")
  23. store.toggleSave(listing)
  24. XCTAssertTrue(store.isSaved(listing))
  25. XCTAssertEqual(store.savedJobs.count, 1)
  26. let reloaded = SwiftDataSavedJobsStore(controller: controller)
  27. XCTAssertTrue(reloaded.isSaved(listing))
  28. XCTAssertEqual(reloaded.savedJobs.first?.listing.title, "iOS Developer")
  29. reloaded.toggleSave(listing)
  30. XCTAssertFalse(reloaded.isSaved(listing))
  31. XCTAssertTrue(reloaded.savedJobs.isEmpty)
  32. }
  33. func testDedupeOnSaveKeepsSingleRow() {
  34. let first = makeListing(key: "samekey000000001", title: "Old Title")
  35. var second = makeListing(key: "samekey000000001", title: "New Title")
  36. second.company = "Acme"
  37. store.toggleSave(first)
  38. let savedAt = store.savedJobs.first?.savedAt
  39. store.save(second)
  40. XCTAssertEqual(store.savedJobs.count, 1)
  41. XCTAssertEqual(store.savedJobs.first?.listing.title, "New Title")
  42. XCTAssertEqual(store.savedJobs.first?.listing.company, "Acme")
  43. XCTAssertEqual(store.savedJobs.first?.savedAt, savedAt)
  44. }
  45. func testUpdateListingPersistsAvailability() {
  46. var listing = makeListing(key: "verifykey0000001", title: "Swift Engineer")
  47. store.toggleSave(listing)
  48. listing.availability = .unavailable
  49. let verifiedAt = Date(timeIntervalSince1970: 1_700_000_000)
  50. store.updateListing(listing, lastVerifiedAt: verifiedAt)
  51. XCTAssertEqual(store.savedJobs.first?.availability, .unavailable)
  52. XCTAssertEqual(store.savedJobs.first?.lastVerifiedAt, verifiedAt)
  53. XCTAssertFalse(store.savedJobs.isEmpty)
  54. }
  55. func testClearAllRemovesPersistedJobs() {
  56. store.toggleSave(makeListing(key: "clear00000000001", title: "A"))
  57. store.toggleSave(makeListing(key: "clear00000000002", title: "B"))
  58. XCTAssertEqual(store.savedJobs.count, 2)
  59. store.clearAll()
  60. XCTAssertTrue(store.savedJobs.isEmpty)
  61. let reloaded = SwiftDataSavedJobsStore(controller: controller)
  62. XCTAssertTrue(reloaded.savedJobs.isEmpty)
  63. }
  64. func testExportJSONContainsKeys() throws {
  65. store.toggleSave(makeListing(key: "exportkey0000001", title: "Export Me"))
  66. let data = try store.exportJSON()
  67. let text = String(data: data, encoding: .utf8) ?? ""
  68. XCTAssertTrue(text.contains("exportkey0000001"))
  69. XCTAssertTrue(text.contains("Export Me"))
  70. }
  71. }
  72. @MainActor
  73. final class SavedJobsRevalidatorTests: XCTestCase {
  74. func testRevalidatesOnlyStaleJobs() async {
  75. let store = MockSavedJobsStore()
  76. var fresh = makeListing(key: "fresh00000000001", title: "Fresh")
  77. fresh.availability = .open
  78. var stale = makeListing(key: "stale00000000001", title: "Stale")
  79. stale.availability = .open
  80. store.toggleSave(fresh)
  81. store.toggleSave(stale)
  82. store.updateListing(fresh, lastVerifiedAt: Date())
  83. store.updateListing(stale, lastVerifiedAt: Date().addingTimeInterval(-86_400 * 2))
  84. let validation = StubValidationService(result: .unavailable)
  85. let revalidator = SavedJobsRevalidator(
  86. store: store,
  87. validationService: validation,
  88. staleInterval: 86_400
  89. )
  90. let checked = await revalidator.runPass()
  91. XCTAssertEqual(checked, 1)
  92. XCTAssertEqual(
  93. store.savedJobs.first(where: { $0.listing.indeedJobKey == "stale00000000001" })?.availability,
  94. .unavailable
  95. )
  96. XCTAssertEqual(
  97. store.savedJobs.first(where: { $0.listing.indeedJobKey == "fresh00000000001" })?.availability,
  98. .open
  99. )
  100. }
  101. func testNeverVerifiedJobsAreChecked() async {
  102. let store = MockSavedJobsStore()
  103. store.toggleSave(makeListing(key: "never00000000001", title: "Never"))
  104. let validation = StubValidationService(result: .unknown)
  105. let revalidator = SavedJobsRevalidator(
  106. store: store,
  107. validationService: validation,
  108. staleInterval: 86_400
  109. )
  110. let checked = await revalidator.runPass()
  111. XCTAssertEqual(checked, 1)
  112. XCTAssertNotNil(store.savedJobs.first?.lastVerifiedAt)
  113. XCTAssertEqual(store.savedJobs.first?.availability, .unknown)
  114. }
  115. }
  116. private struct StubValidationService: JobValidationService {
  117. let result: JobAvailability
  118. func validate(listing: JobListing) async -> JobAvailability {
  119. result
  120. }
  121. }
  122. private func makeListing(
  123. key: String,
  124. title: String,
  125. company: String = "Test Co",
  126. postedAt: Date = Date()
  127. ) -> JobListing {
  128. JobListing(
  129. indeedJobKey: key,
  130. title: title,
  131. company: company,
  132. location: "Remote",
  133. remoteType: .remote,
  134. postedAt: postedAt,
  135. snippet: "Test snippet",
  136. indeedURL: URL(string: "https://www.indeed.com/viewjob?jk=\(key)")!
  137. )
  138. }