diff --git a/Package.resolved b/Package.resolved index bc7ce25..5d87b5e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "eded34b26489ba15446e8f9724a747ccb282c199510abfd2c0945b326d7932a4", "pins" : [ { "identity" : "alamofire", @@ -10,5 +11,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/Sources/ipinfoKit/API/API.swift b/Sources/ipinfoKit/API/API.swift index 07058fa..9fa135e 100644 --- a/Sources/ipinfoKit/API/API.swift +++ b/Sources/ipinfoKit/API/API.swift @@ -37,7 +37,7 @@ extension Service.Router { // MARK: - Service @MainActor -class Service { +public class Service { // MARK: Lifecycle @@ -45,7 +45,7 @@ class Service { // MARK: Internal - static let shared = Service() + public static let shared = Service() var ipInfoURL = "https://ipinfo.io" let headers: HTTPHeaders = [ @@ -53,6 +53,9 @@ class Service { "User-Agent": "IPinfoClient/Swift/\(Constants.SDK_VERSION)", ] + /// Custom session for testing. When nil, uses default AF session. + public var session: Session? + @MainActor func requestAPI( URL: Service.Router, @@ -60,7 +63,8 @@ class Service { params: Parameters? = nil, completion: @escaping (_ status: Response,_ data: Data,_ msg: String) -> Void) { - AF.request(URL.endPoint , method: method, parameters: params , encoding: JSONEncoding.default, headers: headers) + let requestSession = session ?? AF + requestSession.request(URL.endPoint , method: method, parameters: params , encoding: JSONEncoding.default, headers: headers) .response { response in DispatchQueue.main.async { switch response.result { diff --git a/Tests/ipinfoKitTests/ResproxyTests.swift b/Tests/ipinfoKitTests/ResproxyTests.swift index b4c9ba6..6b53422 100644 --- a/Tests/ipinfoKitTests/ResproxyTests.swift +++ b/Tests/ipinfoKitTests/ResproxyTests.swift @@ -1,10 +1,53 @@ +import Alamofire import Foundation import Testing import ipinfoKit +/// Custom URLProtocol that only mocks specific URLs for resproxy tests +private class ResproxyMockURLProtocol: URLProtocol { + nonisolated(unsafe) static var mockResponses: [String: Data] = [:] + + override class func canInit(with request: URLRequest) -> Bool { + guard let url = request.url?.absoluteString else { return false } + return mockResponses.keys.contains(url) + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + guard let url = request.url?.absoluteString, + let data = ResproxyMockURLProtocol.mockResponses[url] else { + client?.urlProtocol(self, didFailWithError: NSError(domain: "ResproxyMockURLProtocol", code: 404)) + return + } + + let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: ["Content-Type": "application/json"])! + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } + + override func stopLoading() {} +} + @MainActor struct ResproxyTests { + @Test func resproxyTest() async throws { + // Set up mock response + let mockData = """ + {"ip":"175.107.211.204","last_seen":"2025-01-20","percent_days_seen":0.85,"service":"example_service"} + """.data(using: .utf8)! + ResproxyMockURLProtocol.mockResponses["https://ipinfo.io/resproxy/175.107.211.204"] = mockData + + // Configure mock session for this test + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [ResproxyMockURLProtocol.self] + let mockSession = Session(configuration: configuration) + Service.shared.session = mockSession + let response = try await withCheckedThrowingContinuation { continuation in IPINFO.shared.getResproxy(ip: "175.107.211.204") { status, response, msg in switch status { @@ -26,13 +69,26 @@ struct ResproxyTests { } } + Service.shared.session = nil + ResproxyMockURLProtocol.mockResponses.removeAll() + #expect(response.ip == "175.107.211.204") - #expect(response.lastSeen != nil) - #expect(response.percentDaysSeen != nil) - #expect(response.service != nil) + #expect(response.lastSeen == "2025-01-20") + #expect(response.percentDaysSeen == 0.85) + #expect(response.service == "example_service") } @Test func resproxyEmptyTest() async throws { + // Set up mock response for empty result + let mockData = "{}".data(using: .utf8)! + ResproxyMockURLProtocol.mockResponses["https://ipinfo.io/resproxy/8.8.8.8"] = mockData + + // Configure mock session for this test + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [ResproxyMockURLProtocol.self] + let mockSession = Session(configuration: configuration) + Service.shared.session = mockSession + let response = try await withCheckedThrowingContinuation { continuation in IPINFO.shared.getResproxy(ip: "8.8.8.8") { status, response, msg in switch status { @@ -54,6 +110,9 @@ struct ResproxyTests { } } + Service.shared.session = nil + ResproxyMockURLProtocol.mockResponses.removeAll() + #expect(response.ip == nil) #expect(response.lastSeen == nil) #expect(response.percentDaysSeen == nil)