restructure
Some checks failed
CI - Multi-Platform Native / Build iOS (RSSuper) (push) Has been cancelled
CI - Multi-Platform Native / Build macOS (push) Has been cancelled
CI - Multi-Platform Native / Build Android (push) Has been cancelled
CI - Multi-Platform Native / Build Linux (push) Has been cancelled
CI - Multi-Platform Native / Build Summary (push) Has been cancelled
Some checks failed
CI - Multi-Platform Native / Build iOS (RSSuper) (push) Has been cancelled
CI - Multi-Platform Native / Build macOS (push) Has been cancelled
CI - Multi-Platform Native / Build Android (push) Has been cancelled
CI - Multi-Platform Native / Build Linux (push) Has been cancelled
CI - Multi-Platform Native / Build Summary (push) Has been cancelled
This commit is contained in:
217
iOS/RSSuperTests/FeedFetcherTests.swift
Normal file
217
iOS/RSSuperTests/FeedFetcherTests.swift
Normal file
@@ -0,0 +1,217 @@
|
||||
import XCTest
|
||||
import os.signpost
|
||||
@testable import RSSuper
|
||||
|
||||
final class FeedFetcherTests: XCTestCase {
|
||||
private var fetcher: FeedFetcher!
|
||||
private var session: MockURLSession!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
session = MockURLSession()
|
||||
fetcher = FeedFetcher(session: session)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
fetcher = nil
|
||||
session = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testFetchFeedSuccess() async throws {
|
||||
let url = URL(string: "https://example.com/feed.xml")!
|
||||
let feedData = Data(rssSample.utf8)
|
||||
|
||||
session.response = (feedData, HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)!)
|
||||
|
||||
let result = try await fetcher.fetchFeed(url: url)
|
||||
|
||||
XCTAssertEqual(result.feedData, feedData)
|
||||
XCTAssertEqual(result.url, url)
|
||||
XCTAssertFalse(result.cached)
|
||||
}
|
||||
|
||||
func testFetchFeedWithAuthentication() async throws {
|
||||
let url = URL(string: "https://example.com/feed.xml")!
|
||||
let feedData = Data(rssSample.utf8)
|
||||
let credentials = HTTPAuthCredentials(username: "user", password: "pass")
|
||||
|
||||
session.response = (feedData, HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)!)
|
||||
|
||||
let result = try await fetcher.fetchFeed(url: url, credentials: credentials)
|
||||
|
||||
XCTAssertEqual(result.feedData, feedData)
|
||||
let authHeader = session.lastAuthHeader
|
||||
XCTAssertTrue(authHeader?.starts(with: "Basic ") ?? false)
|
||||
}
|
||||
|
||||
func testFetchFeedTimeoutError() async throws {
|
||||
let url = URL(string: "https://example.com/feed.xml")!
|
||||
|
||||
session.error = URLError(.timedOut)
|
||||
|
||||
do {
|
||||
_ = try await fetcher.fetchFeed(url: url)
|
||||
XCTFail("Expected error to be thrown")
|
||||
} catch {
|
||||
XCTAssertTrue(true)
|
||||
}
|
||||
}
|
||||
|
||||
func testFetchFeed404Error() async throws {
|
||||
let url = URL(string: "https://example.com/feed.xml")!
|
||||
|
||||
session.response = (Data(), HTTPURLResponse(url: url, statusCode: 404, httpVersion: "HTTP/1.1", headerFields: nil)!)
|
||||
|
||||
do {
|
||||
_ = try await fetcher.fetchFeed(url: url)
|
||||
XCTFail("Expected error to be thrown")
|
||||
} catch {
|
||||
XCTAssertTrue(true)
|
||||
}
|
||||
}
|
||||
|
||||
func testFetchFeedAuthenticationError() async throws {
|
||||
let url = URL(string: "https://example.com/feed.xml")!
|
||||
|
||||
session.response = (Data(), HTTPURLResponse(url: url, statusCode: 401, httpVersion: "HTTP/1.1", headerFields: nil)!)
|
||||
|
||||
do {
|
||||
_ = try await fetcher.fetchFeed(url: url)
|
||||
XCTFail("Expected error to be thrown")
|
||||
} catch {
|
||||
XCTAssertTrue(true)
|
||||
}
|
||||
}
|
||||
|
||||
func testFetchFeedRetriesOnTimeout() async throws {
|
||||
let url = URL(string: "https://example.com/feed.xml")!
|
||||
let feedData = Data(rssSample.utf8)
|
||||
|
||||
var callCount = 0
|
||||
session.onRequest = { [unowned self] in
|
||||
callCount += 1
|
||||
if callCount < 3 {
|
||||
self.session.error = URLError(.timedOut)
|
||||
}
|
||||
}
|
||||
|
||||
session.response = (feedData, HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)!)
|
||||
|
||||
let result = try await fetcher.fetchFeed(url: url)
|
||||
|
||||
XCTAssertEqual(result.feedData, feedData)
|
||||
XCTAssertEqual(callCount, 3)
|
||||
}
|
||||
|
||||
func testFetchFeedCaching() async throws {
|
||||
let url = URL(string: "https://example.com/feed.xml")!
|
||||
let feedData = Data(rssSample.utf8)
|
||||
|
||||
session.response = (feedData, HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)!)
|
||||
|
||||
let result1 = try await fetcher.fetchFeed(url: url)
|
||||
XCTAssertFalse(result1.cached)
|
||||
|
||||
let result2 = try await fetcher.fetchFeed(url: url)
|
||||
XCTAssertTrue(result2.cached)
|
||||
|
||||
XCTAssertEqual(result1.feedData, result2.feedData)
|
||||
}
|
||||
|
||||
func testFetchFeedRespectsCacheControlNoStore() async throws {
|
||||
let url = URL(string: "https://example.com/feed.xml")!
|
||||
let feedData = Data(rssSample.utf8)
|
||||
|
||||
session.response = (feedData, HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: ["Cache-Control": "no-store"])!)
|
||||
|
||||
let result1 = try await fetcher.fetchFeed(url: url)
|
||||
XCTAssertFalse(result1.cached)
|
||||
|
||||
let result2 = try await fetcher.fetchFeed(url: url)
|
||||
XCTAssertFalse(result2.cached)
|
||||
}
|
||||
|
||||
func testHTTPAuthAuthorizationHeader() {
|
||||
let credentials = HTTPAuthCredentials(username: "user", password: "pass")
|
||||
let authHeader = credentials.authorizationHeader()
|
||||
|
||||
XCTAssertEqual(authHeader, "Basic dXNlcjpwYXNz")
|
||||
}
|
||||
|
||||
func testHTTPAuthAuthorizationHeaderWithSpecialCharacters() {
|
||||
let credentials = HTTPAuthCredentials(username: "user@domain", password: "p@ss:w0rd")
|
||||
let authHeader = credentials.authorizationHeader()
|
||||
|
||||
let expectedData = "user@domain:p@ss:w0rd".data(using: .utf8)!
|
||||
let expectedBase64 = expectedData.base64EncodedString()
|
||||
XCTAssertEqual(authHeader, "Basic \(expectedBase64)")
|
||||
}
|
||||
}
|
||||
|
||||
private let rssSample = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>Test Feed</title>
|
||||
<link>https://example.com</link>
|
||||
<description>A test feed</description>
|
||||
<item>
|
||||
<title>Test Item</title>
|
||||
<link>https://example.com/item1</link>
|
||||
<guid>item-1</guid>
|
||||
<description>Test item description</description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
"""
|
||||
|
||||
final class MockURLSession: URLSession {
|
||||
var response: (Data, URLResponse)?
|
||||
var error: Error?
|
||||
var onRequest: (() -> Void)?
|
||||
var lastAuthHeader: String?
|
||||
|
||||
private var requestCounter = 0
|
||||
|
||||
override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
|
||||
requestCounter += 1
|
||||
|
||||
if let auth = request.allHTTPHeaderFields?["Authorization"] {
|
||||
lastAuthHeader = auth
|
||||
}
|
||||
|
||||
onRequest?()
|
||||
|
||||
if let error {
|
||||
completionHandler(nil, nil, error)
|
||||
return MockURLSessionDataTask(error: error)
|
||||
}
|
||||
|
||||
if let (data, response) = response {
|
||||
completionHandler(data, response, nil)
|
||||
return MockURLSessionDataTask()
|
||||
}
|
||||
|
||||
completionHandler(nil, nil, nil)
|
||||
return MockURLSessionDataTask()
|
||||
}
|
||||
|
||||
func reset() {
|
||||
requestCounter = 0
|
||||
lastAuthHeader = nil
|
||||
}
|
||||
}
|
||||
|
||||
final class MockURLSessionDataTask: URLSessionDataTask {
|
||||
private let mockError: Error?
|
||||
|
||||
init(error: Error? = nil) {
|
||||
self.mockError = error
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func resume() {
|
||||
// No-op for testing
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user