IndeedListingMapperTests.swift 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. //
  2. // IndeedListingMapperTests.swift
  3. // indeed_ai_job_searchTests
  4. //
  5. import XCTest
  6. @testable import indeed_ai_job_search
  7. private let searchPage0JSON = """
  8. {
  9. "pageIndex": 0,
  10. "pageSize": 25,
  11. "totalEstimated": 3,
  12. "nextCursor": "page-1",
  13. "hasMore": true,
  14. "results": [
  15. {
  16. "jobKey": "fixture-ios-lahore-001",
  17. "title": "iOS Developer",
  18. "company": "Nexlify Studio",
  19. "location": "Lahore, Pakistan",
  20. "remoteType": "hybrid",
  21. "employmentType": "full-time",
  22. "experienceLevel": "mid",
  23. "salaryMin": 1800000,
  24. "salaryMax": 2800000,
  25. "salaryCurrency": "PKR",
  26. "salaryPeriod": "year",
  27. "postedAt": "2026-10-08T09:30:00Z",
  28. "snippet": "Build SwiftUI features for a consumer productivity app.",
  29. "url": "https://pk.indeed.com/viewjob?jk=fixture-ios-lahore-001",
  30. "availability": "open",
  31. "isEasyApply": true
  32. },
  33. {
  34. "jobKey": "fixture-swift-remote-002",
  35. "title": "Senior Swift Engineer",
  36. "company": "Alpine Cloud",
  37. "location": "Remote, Pakistan",
  38. "remoteType": "remote",
  39. "employmentType": "full-time",
  40. "experienceLevel": "senior",
  41. "salaryMin": 90000,
  42. "salaryMax": 130000,
  43. "salaryCurrency": "USD",
  44. "salaryPeriod": "year",
  45. "postedAt": "2026-10-10T14:00:00Z",
  46. "snippet": "Own macOS and iOS modules.",
  47. "url": "https://www.indeed.com/viewjob?jk=fixture-swift-remote-002",
  48. "availability": "open"
  49. },
  50. {
  51. "title": "Android Developer",
  52. "company": "Karachi Labs",
  53. "location": "Karachi, Pakistan",
  54. "remoteType": "onsite",
  55. "url": "https://pk.indeed.com/viewjob?jk=fixture-android-khi-003",
  56. "availability": "unknown",
  57. "snippet": "Kotlin multiplatform experiments."
  58. }
  59. ]
  60. }
  61. """
  62. final class IndeedListingMapperTests: XCTestCase {
  63. func testMapsFixturePageToJobListings() throws {
  64. let page = try IndeedListingMapper.decodeFixture(from: Data(searchPage0JSON.utf8))
  65. XCTAssertEqual(page.listings.count, 3)
  66. XCTAssertEqual(page.pageIndex, 0)
  67. XCTAssertTrue(page.hasMore)
  68. XCTAssertEqual(page.nextCursor, "page-1")
  69. let first = try XCTUnwrap(page.listings.first)
  70. XCTAssertEqual(first.indeedJobKey, "fixture-ios-lahore-001")
  71. XCTAssertEqual(first.title, "iOS Developer")
  72. XCTAssertEqual(first.remoteType, .hybrid)
  73. XCTAssertEqual(first.employmentType, .fullTime)
  74. XCTAssertEqual(first.experienceLevel, .mid)
  75. XCTAssertEqual(first.salary?.currency, "PKR")
  76. XCTAssertEqual(first.availability, .open)
  77. XCTAssertTrue(IndeedURLCanonicalizer.isIndeedHost(first.indeedURL.host ?? ""))
  78. }
  79. func testExtractsJobKeyFromURLWhenProviderKeyMissing() throws {
  80. let page = try IndeedListingMapper.decodeFixture(from: Data(searchPage0JSON.utf8))
  81. let android = try XCTUnwrap(page.listings.first { $0.title.contains("Android") })
  82. XCTAssertEqual(android.indeedJobKey, "fixture-android-khi-003")
  83. }
  84. func testRejectsNonIndeedURL() {
  85. let dto = IndeedJobDTO(
  86. jobKey: "x",
  87. title: "Role",
  88. company: "Co",
  89. location: "Remote",
  90. url: "https://example.com/jobs/1"
  91. )
  92. XCTAssertThrowsError(try IndeedListingMapper.map(dto)) { error in
  93. guard case IndeedDataError.nonIndeedURL = error else {
  94. return XCTFail("Expected nonIndeedURL, got \(error)")
  95. }
  96. }
  97. }
  98. }
  99. final class IndeedURLCanonicalizerTests: XCTestCase {
  100. func testAcceptsRegionalHosts() throws {
  101. let url = try IndeedURLCanonicalizer.canonicalize("https://pk.indeed.com/viewjob?jk=abc")
  102. XCTAssertEqual(url.host, "pk.indeed.com")
  103. XCTAssertEqual(IndeedURLCanonicalizer.extractJobKey(from: url), "abc")
  104. }
  105. func testAcceptsIndeedCoUK() throws {
  106. let url = try IndeedURLCanonicalizer.canonicalize("http://indeed.co.uk/viewjob?jk=uk1")
  107. XCTAssertEqual(url.scheme, "https")
  108. XCTAssertEqual(url.host, "indeed.co.uk")
  109. }
  110. func testRejectsForeignJobBoard() {
  111. XCTAssertThrowsError(try IndeedURLCanonicalizer.canonicalize("https://linkedin.com/jobs/1"))
  112. }
  113. }
  114. final class FixtureIndeedJobDataSourceTests: XCTestCase {
  115. func testSearchPageContractFiltersByQuery() async throws {
  116. let page = try IndeedListingMapper.decodeFixture(from: Data(searchPage0JSON.utf8))
  117. let source = InlineFixtureIndeedJobDataSource(page: page)
  118. let results = try await source.search(criteria: SearchCriteria(), query: "iOS")
  119. XCTAssertTrue(results.contains { $0.title.localizedCaseInsensitiveContains("iOS") })
  120. }
  121. }
  122. /// Test double that satisfies `IndeedJobDataSource` without depending on app bundle copy phases.
  123. private struct InlineFixtureIndeedJobDataSource: IndeedJobDataSource {
  124. let page: IndeedSearchPage
  125. func searchPage(
  126. criteria: SearchCriteria,
  127. query: String,
  128. page: IndeedSearchPageRequest
  129. ) async throws -> IndeedSearchPage {
  130. var filtered = self.page
  131. let needle = query.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
  132. if !needle.isEmpty {
  133. filtered.listings = filtered.listings.filter {
  134. "\($0.title) \($0.company) \($0.snippet)".lowercased().contains(needle)
  135. }
  136. }
  137. return filtered
  138. }
  139. }