// // IndeedListingMapperTests.swift // indeed_ai_job_searchTests // import XCTest @testable import indeed_ai_job_search private let searchPage0JSON = """ { "pageIndex": 0, "pageSize": 25, "totalEstimated": 3, "nextCursor": "page-1", "hasMore": true, "results": [ { "jobKey": "fixture-ios-lahore-001", "title": "iOS Developer", "company": "Nexlify Studio", "location": "Lahore, Pakistan", "remoteType": "hybrid", "employmentType": "full-time", "experienceLevel": "mid", "salaryMin": 1800000, "salaryMax": 2800000, "salaryCurrency": "PKR", "salaryPeriod": "year", "postedAt": "2026-10-08T09:30:00Z", "snippet": "Build SwiftUI features for a consumer productivity app.", "url": "https://pk.indeed.com/jobs?q=iOS+Developer&l=Lahore%2C+Pakistan", "availability": "open", "isEasyApply": true }, { "jobKey": "fixture-swift-remote-002", "title": "Senior Swift Engineer", "company": "Alpine Cloud", "location": "Remote, Pakistan", "remoteType": "remote", "employmentType": "full-time", "experienceLevel": "senior", "salaryMin": 90000, "salaryMax": 130000, "salaryCurrency": "USD", "salaryPeriod": "year", "postedAt": "2026-10-10T14:00:00Z", "snippet": "Own macOS and iOS modules.", "url": "https://www.indeed.com/jobs?q=Senior+Swift+Engineer&l=Remote", "availability": "open" }, { "title": "Android Developer", "company": "Karachi Labs", "location": "Karachi, Pakistan", "remoteType": "onsite", "url": "https://pk.indeed.com/jobs?q=Android+Developer&l=Karachi%2C+Pakistan", "availability": "unknown", "snippet": "Kotlin multiplatform experiments." } ] } """ final class IndeedListingMapperTests: XCTestCase { func testMapsFixturePageToJobListings() throws { let page = try IndeedListingMapper.decodeFixture(from: Data(searchPage0JSON.utf8)) XCTAssertEqual(page.listings.count, 3) XCTAssertEqual(page.pageIndex, 0) XCTAssertTrue(page.hasMore) XCTAssertEqual(page.nextCursor, "page-1") let first = try XCTUnwrap(page.listings.first) XCTAssertEqual(first.indeedJobKey, "fixture-ios-lahore-001") XCTAssertEqual(first.title, "iOS Developer") XCTAssertEqual(first.remoteType, .hybrid) XCTAssertEqual(first.employmentType, .fullTime) XCTAssertEqual(first.experienceLevel, .mid) XCTAssertEqual(first.salary?.currency, "PKR") XCTAssertEqual(first.availability, .open) XCTAssertTrue(IndeedURLCanonicalizer.isIndeedHost(first.indeedURL.host ?? "")) } func testMapsBrowseURLWhenProviderKeyMissing() throws { let page = try IndeedListingMapper.decodeFixture(from: Data(searchPage0JSON.utf8)) let android = try XCTUnwrap(page.listings.first { $0.title.contains("Android") }) // No provider key + browse URL → stable hash key, openable /jobs URL. XCTAssertTrue(android.indeedJobKey.hasPrefix("url-")) XCTAssertTrue(android.indeedURL.path.contains("/jobs")) XCTAssertTrue(IndeedURLCanonicalizer.isIndeedHost(android.indeedURL.host ?? "")) } func testRejectsNonIndeedURL() { let dto = IndeedJobDTO( jobKey: "x", title: "Role", company: "Co", location: "Remote", url: "https://example.com/jobs/1" ) XCTAssertThrowsError(try IndeedListingMapper.map(dto)) { error in guard case IndeedDataError.nonIndeedURL = error else { return XCTFail("Expected nonIndeedURL, got \(error)") } } } } final class IndeedURLCanonicalizerTests: XCTestCase { func testAcceptsRegionalHosts() throws { let url = try IndeedURLCanonicalizer.canonicalize("https://pk.indeed.com/viewjob?jk=abc") XCTAssertEqual(url.host, "pk.indeed.com") XCTAssertEqual(IndeedURLCanonicalizer.extractJobKey(from: url), "abc") } func testAcceptsIndeedCoUK() throws { let url = try IndeedURLCanonicalizer.canonicalize("http://indeed.co.uk/viewjob?jk=uk1") XCTAssertEqual(url.scheme, "https") XCTAssertEqual(url.host, "indeed.co.uk") } func testRejectsForeignJobBoard() { XCTAssertThrowsError(try IndeedURLCanonicalizer.canonicalize("https://linkedin.com/jobs/1")) } } final class FixtureIndeedJobDataSourceTests: XCTestCase { func testSearchPageContractFiltersByQuery() async throws { let page = try IndeedListingMapper.decodeFixture(from: Data(searchPage0JSON.utf8)) let source = InlineFixtureIndeedJobDataSource(page: page) let results = try await source.search(criteria: SearchCriteria(), query: "iOS") XCTAssertTrue(results.contains { $0.title.localizedCaseInsensitiveContains("iOS") }) } } /// Test double that satisfies `IndeedJobDataSource` without depending on app bundle copy phases. private struct InlineFixtureIndeedJobDataSource: IndeedJobDataSource { let page: IndeedSearchPage func searchPage( criteria: SearchCriteria, query: String, page: IndeedSearchPageRequest ) async throws -> IndeedSearchPage { var filtered = self.page let needle = query.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) if !needle.isEmpty { filtered.listings = filtered.listings.filter { "\($0.title) \($0.company) \($0.snippet)".lowercased().contains(needle) } } return filtered } }