|
|
@@ -0,0 +1,165 @@
|
|
|
+//
|
|
|
+// SavedJobsPersistenceTests.swift
|
|
|
+// indeed_ai_job_searchTests
|
|
|
+//
|
|
|
+
|
|
|
+import XCTest
|
|
|
+@testable import indeed_ai_job_search
|
|
|
+
|
|
|
+@MainActor
|
|
|
+final class SwiftDataSavedJobsStoreTests: XCTestCase {
|
|
|
+ private var controller: PersistenceController!
|
|
|
+ private var store: SwiftDataSavedJobsStore!
|
|
|
+
|
|
|
+ override func setUp() {
|
|
|
+ super.setUp()
|
|
|
+ controller = PersistenceController.makeInMemory()
|
|
|
+ store = SwiftDataSavedJobsStore(controller: controller)
|
|
|
+ }
|
|
|
+
|
|
|
+ override func tearDown() {
|
|
|
+ store = nil
|
|
|
+ controller = nil
|
|
|
+ super.tearDown()
|
|
|
+ }
|
|
|
+
|
|
|
+ func testSaveUnsaveSurvivesReload() {
|
|
|
+ let listing = makeListing(key: "abc123def4567890", title: "iOS Developer")
|
|
|
+ store.toggleSave(listing)
|
|
|
+ XCTAssertTrue(store.isSaved(listing))
|
|
|
+ XCTAssertEqual(store.savedJobs.count, 1)
|
|
|
+
|
|
|
+ let reloaded = SwiftDataSavedJobsStore(controller: controller)
|
|
|
+ XCTAssertTrue(reloaded.isSaved(listing))
|
|
|
+ XCTAssertEqual(reloaded.savedJobs.first?.listing.title, "iOS Developer")
|
|
|
+
|
|
|
+ reloaded.toggleSave(listing)
|
|
|
+ XCTAssertFalse(reloaded.isSaved(listing))
|
|
|
+ XCTAssertTrue(reloaded.savedJobs.isEmpty)
|
|
|
+ }
|
|
|
+
|
|
|
+ func testDedupeOnSaveKeepsSingleRow() {
|
|
|
+ let first = makeListing(key: "samekey000000001", title: "Old Title")
|
|
|
+ var second = makeListing(key: "samekey000000001", title: "New Title")
|
|
|
+ second.company = "Acme"
|
|
|
+
|
|
|
+ store.toggleSave(first)
|
|
|
+ let savedAt = store.savedJobs.first?.savedAt
|
|
|
+ store.save(second)
|
|
|
+
|
|
|
+ XCTAssertEqual(store.savedJobs.count, 1)
|
|
|
+ XCTAssertEqual(store.savedJobs.first?.listing.title, "New Title")
|
|
|
+ XCTAssertEqual(store.savedJobs.first?.listing.company, "Acme")
|
|
|
+ XCTAssertEqual(store.savedJobs.first?.savedAt, savedAt)
|
|
|
+ }
|
|
|
+
|
|
|
+ func testUpdateListingPersistsAvailability() {
|
|
|
+ var listing = makeListing(key: "verifykey0000001", title: "Swift Engineer")
|
|
|
+ store.toggleSave(listing)
|
|
|
+ listing.availability = .unavailable
|
|
|
+
|
|
|
+ let verifiedAt = Date(timeIntervalSince1970: 1_700_000_000)
|
|
|
+ store.updateListing(listing, lastVerifiedAt: verifiedAt)
|
|
|
+
|
|
|
+ XCTAssertEqual(store.savedJobs.first?.availability, .unavailable)
|
|
|
+ XCTAssertEqual(store.savedJobs.first?.lastVerifiedAt, verifiedAt)
|
|
|
+ XCTAssertFalse(store.savedJobs.isEmpty)
|
|
|
+ }
|
|
|
+
|
|
|
+ func testClearAllRemovesPersistedJobs() {
|
|
|
+ store.toggleSave(makeListing(key: "clear00000000001", title: "A"))
|
|
|
+ store.toggleSave(makeListing(key: "clear00000000002", title: "B"))
|
|
|
+ XCTAssertEqual(store.savedJobs.count, 2)
|
|
|
+
|
|
|
+ store.clearAll()
|
|
|
+ XCTAssertTrue(store.savedJobs.isEmpty)
|
|
|
+
|
|
|
+ let reloaded = SwiftDataSavedJobsStore(controller: controller)
|
|
|
+ XCTAssertTrue(reloaded.savedJobs.isEmpty)
|
|
|
+ }
|
|
|
+
|
|
|
+ func testExportJSONContainsKeys() throws {
|
|
|
+ store.toggleSave(makeListing(key: "exportkey0000001", title: "Export Me"))
|
|
|
+ let data = try store.exportJSON()
|
|
|
+ let text = String(data: data, encoding: .utf8) ?? ""
|
|
|
+ XCTAssertTrue(text.contains("exportkey0000001"))
|
|
|
+ XCTAssertTrue(text.contains("Export Me"))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@MainActor
|
|
|
+final class SavedJobsRevalidatorTests: XCTestCase {
|
|
|
+ func testRevalidatesOnlyStaleJobs() async {
|
|
|
+ let store = MockSavedJobsStore()
|
|
|
+ var fresh = makeListing(key: "fresh00000000001", title: "Fresh")
|
|
|
+ fresh.availability = .open
|
|
|
+ var stale = makeListing(key: "stale00000000001", title: "Stale")
|
|
|
+ stale.availability = .open
|
|
|
+
|
|
|
+ store.toggleSave(fresh)
|
|
|
+ store.toggleSave(stale)
|
|
|
+ store.updateListing(fresh, lastVerifiedAt: Date())
|
|
|
+ store.updateListing(stale, lastVerifiedAt: Date().addingTimeInterval(-86_400 * 2))
|
|
|
+
|
|
|
+ let validation = StubValidationService(result: .unavailable)
|
|
|
+ let revalidator = SavedJobsRevalidator(
|
|
|
+ store: store,
|
|
|
+ validationService: validation,
|
|
|
+ staleInterval: 86_400
|
|
|
+ )
|
|
|
+
|
|
|
+ let checked = await revalidator.runPass()
|
|
|
+ XCTAssertEqual(checked, 1)
|
|
|
+ XCTAssertEqual(
|
|
|
+ store.savedJobs.first(where: { $0.listing.indeedJobKey == "stale00000000001" })?.availability,
|
|
|
+ .unavailable
|
|
|
+ )
|
|
|
+ XCTAssertEqual(
|
|
|
+ store.savedJobs.first(where: { $0.listing.indeedJobKey == "fresh00000000001" })?.availability,
|
|
|
+ .open
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ func testNeverVerifiedJobsAreChecked() async {
|
|
|
+ let store = MockSavedJobsStore()
|
|
|
+ store.toggleSave(makeListing(key: "never00000000001", title: "Never"))
|
|
|
+
|
|
|
+ let validation = StubValidationService(result: .unknown)
|
|
|
+ let revalidator = SavedJobsRevalidator(
|
|
|
+ store: store,
|
|
|
+ validationService: validation,
|
|
|
+ staleInterval: 86_400
|
|
|
+ )
|
|
|
+
|
|
|
+ let checked = await revalidator.runPass()
|
|
|
+ XCTAssertEqual(checked, 1)
|
|
|
+ XCTAssertNotNil(store.savedJobs.first?.lastVerifiedAt)
|
|
|
+ XCTAssertEqual(store.savedJobs.first?.availability, .unknown)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private struct StubValidationService: JobValidationService {
|
|
|
+ let result: JobAvailability
|
|
|
+
|
|
|
+ func validate(listing: JobListing) async -> JobAvailability {
|
|
|
+ result
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private func makeListing(
|
|
|
+ key: String,
|
|
|
+ title: String,
|
|
|
+ company: String = "Test Co",
|
|
|
+ postedAt: Date = Date()
|
|
|
+) -> JobListing {
|
|
|
+ JobListing(
|
|
|
+ indeedJobKey: key,
|
|
|
+ title: title,
|
|
|
+ company: company,
|
|
|
+ location: "Remote",
|
|
|
+ remoteType: .remote,
|
|
|
+ postedAt: postedAt,
|
|
|
+ snippet: "Test snippet",
|
|
|
+ indeedURL: URL(string: "https://www.indeed.com/viewjob?jk=\(key)")!
|
|
|
+ )
|
|
|
+}
|