IndeedListingMapperTests.swift 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  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/jobs?q=iOS+Developer&l=Lahore%2C+Pakistan",
  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/jobs?q=Senior+Swift+Engineer&l=Remote",
  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/jobs?q=Android+Developer&l=Karachi%2C+Pakistan",
  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 testMapsBrowseURLWhenProviderKeyMissing() throws {
  80. let page = try IndeedListingMapper.decodeFixture(from: Data(searchPage0JSON.utf8))
  81. let android = try XCTUnwrap(page.listings.first { $0.title.contains("Android") })
  82. // No provider key + browse URL → stable hash key, openable /jobs URL.
  83. XCTAssertTrue(android.indeedJobKey.hasPrefix("url-"))
  84. XCTAssertTrue(android.indeedURL.path.contains("/jobs"))
  85. XCTAssertTrue(IndeedURLCanonicalizer.isIndeedHost(android.indeedURL.host ?? ""))
  86. }
  87. func testRejectsNonIndeedURL() {
  88. let dto = IndeedJobDTO(
  89. jobKey: "x",
  90. title: "Role",
  91. company: "Co",
  92. location: "Remote",
  93. url: "https://example.com/jobs/1"
  94. )
  95. XCTAssertThrowsError(try IndeedListingMapper.map(dto)) { error in
  96. guard case IndeedDataError.nonIndeedURL = error else {
  97. return XCTFail("Expected nonIndeedURL, got \(error)")
  98. }
  99. }
  100. }
  101. }
  102. final class IndeedURLCanonicalizerTests: XCTestCase {
  103. func testAcceptsRegionalHosts() throws {
  104. let url = try IndeedURLCanonicalizer.canonicalize("https://pk.indeed.com/viewjob?jk=abc")
  105. XCTAssertEqual(url.host, "pk.indeed.com")
  106. XCTAssertEqual(IndeedURLCanonicalizer.extractJobKey(from: url), "abc")
  107. }
  108. func testAcceptsIndeedCoUK() throws {
  109. let url = try IndeedURLCanonicalizer.canonicalize("http://indeed.co.uk/viewjob?jk=uk1")
  110. XCTAssertEqual(url.scheme, "https")
  111. XCTAssertEqual(url.host, "indeed.co.uk")
  112. }
  113. func testRejectsForeignJobBoard() {
  114. XCTAssertThrowsError(try IndeedURLCanonicalizer.canonicalize("https://linkedin.com/jobs/1"))
  115. }
  116. }
  117. final class FixtureIndeedJobDataSourceTests: XCTestCase {
  118. func testSearchPageContractFiltersByQuery() async throws {
  119. let page = try IndeedListingMapper.decodeFixture(from: Data(searchPage0JSON.utf8))
  120. let source = InlineFixtureIndeedJobDataSource(page: page)
  121. let results = try await source.search(criteria: SearchCriteria(), query: "iOS")
  122. XCTAssertTrue(results.contains { $0.title.localizedCaseInsensitiveContains("iOS") })
  123. }
  124. }
  125. /// Test double that satisfies `IndeedJobDataSource` without depending on app bundle copy phases.
  126. private struct InlineFixtureIndeedJobDataSource: IndeedJobDataSource {
  127. let page: IndeedSearchPage
  128. func searchPage(
  129. criteria: SearchCriteria,
  130. query: String,
  131. page: IndeedSearchPageRequest
  132. ) async throws -> IndeedSearchPage {
  133. var filtered = self.page
  134. let needle = query.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
  135. if !needle.isEmpty {
  136. filtered.listings = filtered.listings.filter {
  137. "\($0.title) \($0.company) \($0.snippet)".lowercased().contains(needle)
  138. }
  139. }
  140. return filtered
  141. }
  142. }