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:
395
iOS/RSSuperTests/DatabaseManagerTests.swift
Normal file
395
iOS/RSSuperTests/DatabaseManagerTests.swift
Normal file
@@ -0,0 +1,395 @@
|
||||
//
|
||||
// DatabaseManagerTests.swift
|
||||
// RSSuperTests
|
||||
//
|
||||
// Created on 3/29/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import RSSuper
|
||||
|
||||
final class DatabaseManagerTests: XCTestCase {
|
||||
|
||||
private var databaseManager: DatabaseManager!
|
||||
private var testSubscriptionId: String!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
databaseManager = DatabaseManager.shared
|
||||
testSubscriptionId = UUID().uuidString
|
||||
|
||||
// Clean up any existing test data
|
||||
try? databaseManager.deleteSubscription(id: testSubscriptionId)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Clean up test data
|
||||
try? databaseManager.deleteSubscription(id: testSubscriptionId)
|
||||
databaseManager = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - Subscription CRUD Tests
|
||||
|
||||
func testCreateSubscription() throws {
|
||||
let subscription = try databaseManager.createSubscription(
|
||||
id: testSubscriptionId,
|
||||
url: "https://example.com/feed.xml",
|
||||
title: "Test Subscription",
|
||||
category: "Technology",
|
||||
enabled: true,
|
||||
fetchInterval: 3600
|
||||
)
|
||||
|
||||
XCTAssertEqual(subscription.id, testSubscriptionId)
|
||||
XCTAssertEqual(subscription.url, "https://example.com/feed.xml")
|
||||
XCTAssertEqual(subscription.title, "Test Subscription")
|
||||
XCTAssertEqual(subscription.category, "Technology")
|
||||
XCTAssertTrue(subscription.enabled)
|
||||
XCTAssertEqual(subscription.fetchInterval, 3600)
|
||||
}
|
||||
|
||||
func testFetchSubscription() throws {
|
||||
// Create first
|
||||
_ = try databaseManager.createSubscription(
|
||||
id: testSubscriptionId,
|
||||
url: "https://example.com/feed.xml",
|
||||
title: "Test Subscription"
|
||||
)
|
||||
|
||||
// Fetch
|
||||
let fetched = try databaseManager.fetchSubscription(id: testSubscriptionId)
|
||||
|
||||
XCTAssertNotNil(fetched)
|
||||
XCTAssertEqual(fetched?.id, testSubscriptionId)
|
||||
XCTAssertEqual(fetched?.title, "Test Subscription")
|
||||
}
|
||||
|
||||
func testFetchSubscriptionNotFound() throws {
|
||||
let fetched = try databaseManager.fetchSubscription(id: "non-existent-id")
|
||||
XCTAssertNil(fetched)
|
||||
}
|
||||
|
||||
func testFetchAllSubscriptions() throws {
|
||||
let id1 = UUID().uuidString
|
||||
let id2 = UUID().uuidString
|
||||
|
||||
try databaseManager.createSubscription(id: id1, url: "https://example1.com", title: "Sub 1")
|
||||
try databaseManager.createSubscription(id: id2, url: "https://example2.com", title: "Sub 2")
|
||||
|
||||
let subscriptions = try databaseManager.fetchAllSubscriptions()
|
||||
|
||||
XCTAssertGreaterThanOrEqual(subscriptions.count, 2)
|
||||
|
||||
// Cleanup
|
||||
try databaseManager.deleteSubscription(id: id1)
|
||||
try databaseManager.deleteSubscription(id: id2)
|
||||
}
|
||||
|
||||
func testFetchEnabledSubscriptions() throws {
|
||||
let id1 = UUID().uuidString
|
||||
let id2 = UUID().uuidString
|
||||
|
||||
try databaseManager.createSubscription(id: id1, url: "https://example1.com", title: "Sub 1", enabled: true)
|
||||
try databaseManager.createSubscription(id: id2, url: "https://example2.com", title: "Sub 2", enabled: false)
|
||||
|
||||
let subscriptions = try databaseManager.fetchEnabledSubscriptions()
|
||||
|
||||
XCTAssertTrue(subscriptions.allSatisfy { $0.enabled })
|
||||
|
||||
// Cleanup
|
||||
try databaseManager.deleteSubscription(id: id1)
|
||||
try databaseManager.deleteSubscription(id: id2)
|
||||
}
|
||||
|
||||
func testUpdateSubscription() throws {
|
||||
_ = try databaseManager.createSubscription(
|
||||
id: testSubscriptionId,
|
||||
url: "https://example.com",
|
||||
title: "Original Title"
|
||||
)
|
||||
|
||||
let updated = try databaseManager.updateSubscription(
|
||||
try databaseManager.fetchSubscription(id: testSubscriptionId)!,
|
||||
title: "Updated Title",
|
||||
enabled: false
|
||||
)
|
||||
|
||||
XCTAssertEqual(updated.title, "Updated Title")
|
||||
XCTAssertFalse(updated.enabled)
|
||||
}
|
||||
|
||||
func testDeleteSubscription() throws {
|
||||
_ = try databaseManager.createSubscription(
|
||||
id: testSubscriptionId,
|
||||
url: "https://example.com",
|
||||
title: "To Delete"
|
||||
)
|
||||
|
||||
try databaseManager.deleteSubscription(id: testSubscriptionId)
|
||||
|
||||
let fetched = try databaseManager.fetchSubscription(id: testSubscriptionId)
|
||||
XCTAssertNil(fetched)
|
||||
}
|
||||
|
||||
// MARK: - FeedItem CRUD Tests
|
||||
|
||||
func testCreateFeedItem() throws {
|
||||
let subscription = try databaseManager.createSubscription(
|
||||
id: testSubscriptionId,
|
||||
url: "https://example.com",
|
||||
title: "Test Sub"
|
||||
)
|
||||
|
||||
let item = FeedItem(
|
||||
id: UUID().uuidString,
|
||||
title: "Test Article",
|
||||
link: "https://example.com/article",
|
||||
description: "Article description",
|
||||
content: "Full article content",
|
||||
author: "John Doe",
|
||||
published: Date(),
|
||||
subscriptionId: subscription.id,
|
||||
subscriptionTitle: subscription.title
|
||||
)
|
||||
|
||||
let created = try databaseManager.createFeedItem(item)
|
||||
|
||||
XCTAssertEqual(created.id, item.id)
|
||||
XCTAssertEqual(created.title, "Test Article")
|
||||
XCTAssertEqual(created.subscriptionId, testSubscriptionId)
|
||||
}
|
||||
|
||||
func testFetchFeedItem() throws {
|
||||
let subscription = try databaseManager.createSubscription(
|
||||
id: testSubscriptionId,
|
||||
url: "https://example.com",
|
||||
title: "Test Sub"
|
||||
)
|
||||
|
||||
let itemId = UUID().uuidString
|
||||
let item = FeedItem(
|
||||
id: itemId,
|
||||
title: "To Fetch",
|
||||
subscriptionId: subscription.id
|
||||
)
|
||||
_ = try databaseManager.createFeedItem(item)
|
||||
|
||||
let fetched = try databaseManager.fetchFeedItem(id: itemId)
|
||||
|
||||
XCTAssertNotNil(fetched)
|
||||
XCTAssertEqual(fetched?.id, itemId)
|
||||
}
|
||||
|
||||
func testFetchFeedItemsForSubscription() throws {
|
||||
let subscription = try databaseManager.createSubscription(
|
||||
id: testSubscriptionId,
|
||||
url: "https://example.com",
|
||||
title: "Test Sub"
|
||||
)
|
||||
|
||||
for i in 1...3 {
|
||||
let item = FeedItem(
|
||||
id: UUID().uuidString,
|
||||
title: "Article \(i)",
|
||||
subscriptionId: subscription.id
|
||||
)
|
||||
_ = try databaseManager.createFeedItem(item)
|
||||
}
|
||||
|
||||
let items = try databaseManager.fetchFeedItems(for: testSubscriptionId)
|
||||
|
||||
XCTAssertEqual(items.count, 3)
|
||||
}
|
||||
|
||||
func testFetchUnreadFeedItems() throws {
|
||||
let subscription = try databaseManager.createSubscription(
|
||||
id: testSubscriptionId,
|
||||
url: "https://example.com",
|
||||
title: "Test Sub"
|
||||
)
|
||||
|
||||
let readItem = FeedItem(id: UUID().uuidString, title: "Read", subscriptionId: subscription.id, read: true)
|
||||
let unreadItem = FeedItem(id: UUID().uuidString, title: "Unread", subscriptionId: subscription.id, read: false)
|
||||
|
||||
_ = try databaseManager.createFeedItem(readItem)
|
||||
_ = try databaseManager.createFeedItem(unreadItem)
|
||||
|
||||
let unreadItems = try databaseManager.fetchUnreadFeedItems()
|
||||
|
||||
XCTAssertTrue(unreadItems.allSatisfy { !$0.read })
|
||||
}
|
||||
|
||||
func testMarkAsRead() throws {
|
||||
let subscription = try databaseManager.createSubscription(
|
||||
id: testSubscriptionId,
|
||||
url: "https://example.com",
|
||||
title: "Test Sub"
|
||||
)
|
||||
|
||||
let item1 = FeedItem(id: UUID().uuidString, title: "Item 1", subscriptionId: subscription.id, read: false)
|
||||
let item2 = FeedItem(id: UUID().uuidString, title: "Item 2", subscriptionId: subscription.id, read: false)
|
||||
|
||||
_ = try databaseManager.createFeedItem(item1)
|
||||
_ = try databaseManager.createFeedItem(item2)
|
||||
|
||||
try databaseManager.markAsRead(ids: [item1.id, item2.id])
|
||||
|
||||
let fetched1 = try databaseManager.fetchFeedItem(id: item1.id)
|
||||
let fetched2 = try databaseManager.fetchFeedItem(id: item2.id)
|
||||
|
||||
XCTAssertTrue(fetched1?.read ?? false)
|
||||
XCTAssertTrue(fetched2?.read ?? false)
|
||||
}
|
||||
|
||||
func testDeleteFeedItem() throws {
|
||||
let subscription = try databaseManager.createSubscription(
|
||||
id: testSubscriptionId,
|
||||
url: "https://example.com",
|
||||
title: "Test Sub"
|
||||
)
|
||||
|
||||
let item = FeedItem(id: UUID().uuidString, title: "To Delete", subscriptionId: subscription.id)
|
||||
_ = try databaseManager.createFeedItem(item)
|
||||
|
||||
try databaseManager.deleteFeedItem(id: item.id)
|
||||
|
||||
let fetched = try databaseManager.fetchFeedItem(id: item.id)
|
||||
XCTAssertNil(fetched)
|
||||
}
|
||||
|
||||
// MARK: - SearchHistory Tests
|
||||
|
||||
func testAddToSearchHistory() throws {
|
||||
let item = try databaseManager.addToSearchHistory(query: "test query")
|
||||
|
||||
XCTAssertEqual(item.query, "test query")
|
||||
XCTAssertNotNil(item.id)
|
||||
}
|
||||
|
||||
func testFetchSearchHistory() throws {
|
||||
try databaseManager.addToSearchHistory(query: "Query 1")
|
||||
try databaseManager.addToSearchHistory(query: "Query 2")
|
||||
|
||||
let history = try databaseManager.fetchSearchHistory()
|
||||
|
||||
XCTAssertGreaterThanOrEqual(history.count, 2)
|
||||
}
|
||||
|
||||
func testClearSearchHistory() throws {
|
||||
try databaseManager.addToSearchHistory(query: "To Clear")
|
||||
try databaseManager.clearSearchHistory()
|
||||
|
||||
let history = try databaseManager.fetchSearchHistory()
|
||||
XCTAssertTrue(history.isEmpty)
|
||||
}
|
||||
|
||||
// MARK: - FTS Search Tests
|
||||
|
||||
func testFullTextSearch() throws {
|
||||
let subscription = try databaseManager.createSubscription(
|
||||
id: testSubscriptionId,
|
||||
url: "https://example.com",
|
||||
title: "Test Sub"
|
||||
)
|
||||
|
||||
let searchableItem = FeedItem(
|
||||
id: UUID().uuidString,
|
||||
title: "Unique Title for Search Test",
|
||||
description: "This has special content",
|
||||
subscriptionId: subscription.id
|
||||
)
|
||||
let nonMatchingItem = FeedItem(
|
||||
id: UUID().uuidString,
|
||||
title: "Completely Different",
|
||||
subscriptionId: subscription.id
|
||||
)
|
||||
|
||||
_ = try databaseManager.createFeedItem(searchableItem)
|
||||
_ = try databaseManager.createFeedItem(nonMatchingItem)
|
||||
|
||||
let results = try databaseManager.fullTextSearch(query: "Unique")
|
||||
|
||||
XCTAssertTrue(results.contains { $0.id == searchableItem.id || $0.title.contains("Unique") })
|
||||
XCTAssertFalse(results.contains { $0.id == nonMatchingItem.id })
|
||||
}
|
||||
|
||||
func testAdvancedSearch() throws {
|
||||
let subscription = try databaseManager.createSubscription(
|
||||
id: testSubscriptionId,
|
||||
url: "https://example.com",
|
||||
title: "Test Sub"
|
||||
)
|
||||
|
||||
let item1 = FeedItem(
|
||||
id: UUID().uuidString,
|
||||
title: "Searchable Title",
|
||||
author: "Test Author",
|
||||
subscriptionId: subscription.id
|
||||
)
|
||||
let item2 = FeedItem(
|
||||
id: UUID().uuidString,
|
||||
title: "Different Title",
|
||||
author: "Other Author",
|
||||
subscriptionId: subscription.id
|
||||
)
|
||||
|
||||
_ = try databaseManager.createFeedItem(item1)
|
||||
_ = try databaseManager.createFeedItem(item2)
|
||||
|
||||
let results = try databaseManager.advancedSearch(title: "Searchable", author: "Test")
|
||||
|
||||
XCTAssertTrue(results.contains { $0.id == item1.id })
|
||||
XCTAssertFalse(results.contains { $0.id == item2.id })
|
||||
}
|
||||
|
||||
// MARK: - Batch Operations Tests
|
||||
|
||||
func testMarkAllAsRead() throws {
|
||||
let subscription = try databaseManager.createSubscription(
|
||||
id: testSubscriptionId,
|
||||
url: "https://example.com",
|
||||
title: "Test Sub"
|
||||
)
|
||||
|
||||
_ = try databaseManager.createFeedItem(FeedItem(id: UUID().uuidString, title: "Item 1", subscriptionId: subscription.id, read: false))
|
||||
_ = try databaseManager.createFeedItem(FeedItem(id: UUID().uuidString, title: "Item 2", subscriptionId: subscription.id, read: false))
|
||||
|
||||
try databaseManager.markAllAsRead(for: testSubscriptionId)
|
||||
|
||||
let items = try databaseManager.fetchFeedItems(for: testSubscriptionId)
|
||||
XCTAssertTrue(items.allSatisfy { $0.read })
|
||||
}
|
||||
|
||||
func testCleanupOldItems() throws {
|
||||
let subscription = try databaseManager.createSubscription(
|
||||
id: testSubscriptionId,
|
||||
url: "https://example.com",
|
||||
title: "Test Sub"
|
||||
)
|
||||
|
||||
let oldItem = FeedItem(
|
||||
id: UUID().uuidString,
|
||||
title: "Old Item",
|
||||
published: Calendar.current.date(byAdding: .day, value: -30, to: Date()),
|
||||
subscriptionId: subscription.id
|
||||
)
|
||||
let newItem = FeedItem(
|
||||
id: UUID().uuidString,
|
||||
title: "New Item",
|
||||
published: Date(),
|
||||
subscriptionId: subscription.id
|
||||
)
|
||||
|
||||
_ = try databaseManager.createFeedItem(oldItem)
|
||||
_ = try databaseManager.createFeedItem(newItem)
|
||||
|
||||
let deletedCount = try databaseManager.cleanupOldItems(olderThan: 7, for: testSubscriptionId)
|
||||
|
||||
XCTAssertEqual(deletedCount, 1)
|
||||
|
||||
let remainingItems = try databaseManager.fetchFeedItems(for: testSubscriptionId)
|
||||
XCTAssertEqual(remainingItems.count, 1)
|
||||
XCTAssertEqual(remainingItems.first?.id, newItem.id)
|
||||
}
|
||||
}
|
||||
56
iOS/RSSuperTests/DateExtensionsTests.swift
Normal file
56
iOS/RSSuperTests/DateExtensionsTests.swift
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// DateExtensionsTests.swift
|
||||
// RSSuperTests
|
||||
//
|
||||
// Created by Mike Freno on 3/29/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import RSSuper
|
||||
|
||||
final class DateExtensionsTests: XCTestCase {
|
||||
|
||||
func testMillisecondsSince1970() {
|
||||
let date = Date(timeIntervalSince1970: 1609459200)
|
||||
XCTAssertEqual(date.millisecondsSince1970, 1609459200000)
|
||||
}
|
||||
|
||||
func testInitFromMilliseconds() {
|
||||
let date = Date(millisecondsSince1970: 1609459200000)!
|
||||
XCTAssertEqual(date.timeIntervalSince1970, 1609459200)
|
||||
}
|
||||
|
||||
func testSecondsSince1970() {
|
||||
let date = Date(timeIntervalSince1970: 1609459200)
|
||||
XCTAssertEqual(date.secondsSince1970, 1609459200)
|
||||
}
|
||||
|
||||
func testInitFromSeconds() {
|
||||
let date = Date(secondsSince1970: 1609459200)!
|
||||
XCTAssertEqual(date.timeIntervalSince1970, 1609459200)
|
||||
}
|
||||
|
||||
func testISO8601String() {
|
||||
let date = Date(timeIntervalSince1970: 1609459200)
|
||||
let isoString = date.iso8601String
|
||||
|
||||
// Verify it's a valid ISO8601 string
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
XCTAssertNotNil(formatter.date(from: isoString))
|
||||
}
|
||||
|
||||
func testInitFromISO8601String() {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
let isoString = formatter.string(from: Date(timeIntervalSince1970: 1609459200))
|
||||
|
||||
let date = Date(iso8601String: isoString)!
|
||||
XCTAssertEqual(date.timeIntervalSince1970, 1609459200, accuracy: 1.0)
|
||||
}
|
||||
|
||||
func testInvalidISO8601String() {
|
||||
let date = Date(iso8601String: "not-a-date")
|
||||
XCTAssertNil(date)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
148
iOS/RSSuperTests/FeedItemTests.swift
Normal file
148
iOS/RSSuperTests/FeedItemTests.swift
Normal file
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// FeedItemTests.swift
|
||||
// RSSuperTests
|
||||
//
|
||||
// Created by Mike Freno on 3/29/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import RSSuper
|
||||
|
||||
final class FeedItemTests: XCTestCase {
|
||||
|
||||
func testFeedItemEncodingDecoding() throws {
|
||||
let item = FeedItem(
|
||||
id: "550e8400-e29b-41d4-a716-446655440000",
|
||||
title: "Test Article",
|
||||
link: "https://example.com/article",
|
||||
description: "A test description",
|
||||
content: "Full content here",
|
||||
author: "John Doe",
|
||||
published: Date(millisecondsSince1970: 1609459200000),
|
||||
categories: ["tech", "swift"],
|
||||
enclosure: Enclosure(url: "https://example.com/audio.mp3", type: "audio/mpeg", length: 1234567),
|
||||
guid: "guid-123",
|
||||
subscriptionId: "sub-123",
|
||||
subscriptionTitle: "Test Feed"
|
||||
)
|
||||
|
||||
let data = try JSONEncoder().encode(item)
|
||||
let decoded = try JSONDecoder().decode(FeedItem.self, from: data)
|
||||
|
||||
XCTAssertEqual(decoded.id, item.id)
|
||||
XCTAssertEqual(decoded.title, item.title)
|
||||
XCTAssertEqual(decoded.link, item.link)
|
||||
XCTAssertEqual(decoded.description, item.description)
|
||||
XCTAssertEqual(decoded.content, item.content)
|
||||
XCTAssertEqual(decoded.author, item.author)
|
||||
XCTAssertEqual(decoded.published, item.published)
|
||||
XCTAssertEqual(decoded.categories, item.categories)
|
||||
XCTAssertEqual(decoded.guid, item.guid)
|
||||
XCTAssertEqual(decoded.subscriptionId, item.subscriptionId)
|
||||
XCTAssertEqual(decoded.subscriptionTitle, item.subscriptionTitle)
|
||||
}
|
||||
|
||||
func testFeedItemOptionalProperties() throws {
|
||||
let item = FeedItem(
|
||||
id: "test-id",
|
||||
title: "Minimal Item",
|
||||
subscriptionId: "sub-id"
|
||||
)
|
||||
|
||||
XCTAssertNil(item.link)
|
||||
XCTAssertNil(item.description)
|
||||
XCTAssertNil(item.content)
|
||||
XCTAssertNil(item.author)
|
||||
XCTAssertNil(item.published)
|
||||
XCTAssertNil(item.categories)
|
||||
XCTAssertNil(item.enclosure)
|
||||
XCTAssertNil(item.guid)
|
||||
XCTAssertNil(item.subscriptionTitle)
|
||||
|
||||
let data = try JSONEncoder().encode(item)
|
||||
let decoded = try JSONDecoder().decode(FeedItem.self, from: data)
|
||||
|
||||
XCTAssertEqual(decoded.id, item.id)
|
||||
XCTAssertEqual(decoded.title, item.title)
|
||||
}
|
||||
|
||||
func testEnclosureEncodingDecoding() throws {
|
||||
let enclosure = Enclosure(
|
||||
url: "https://example.com/podcast.mp3",
|
||||
type: "audio/mpeg",
|
||||
length: 98765432
|
||||
)
|
||||
|
||||
let data = try JSONEncoder().encode(enclosure)
|
||||
let decoded = try JSONDecoder().decode(Enclosure.self, from: data)
|
||||
|
||||
XCTAssertEqual(decoded.url, enclosure.url)
|
||||
XCTAssertEqual(decoded.type, enclosure.type)
|
||||
XCTAssertEqual(decoded.length, enclosure.length)
|
||||
}
|
||||
|
||||
func testEnclosureWithoutLength() throws {
|
||||
let enclosure = Enclosure(
|
||||
url: "https://example.com/video.mp4",
|
||||
type: "video/mp4"
|
||||
)
|
||||
|
||||
let data = try JSONEncoder().encode(enclosure)
|
||||
let decoded = try JSONDecoder().decode(Enclosure.self, from: data)
|
||||
|
||||
XCTAssertEqual(decoded.url, enclosure.url)
|
||||
XCTAssertEqual(decoded.type, enclosure.type)
|
||||
XCTAssertNil(decoded.length)
|
||||
}
|
||||
|
||||
func testFeedItemEquality() {
|
||||
let item1 = FeedItem(
|
||||
id: "same-id",
|
||||
title: "Same Title",
|
||||
subscriptionId: "sub-id"
|
||||
)
|
||||
|
||||
let item2 = FeedItem(
|
||||
id: "same-id",
|
||||
title: "Same Title",
|
||||
subscriptionId: "sub-id"
|
||||
)
|
||||
|
||||
let item3 = FeedItem(
|
||||
id: "different-id",
|
||||
title: "Different Title",
|
||||
subscriptionId: "sub-id"
|
||||
)
|
||||
|
||||
XCTAssertEqual(item1, item2)
|
||||
XCTAssertNotEqual(item1, item3)
|
||||
}
|
||||
|
||||
func testContentTypeDetection() {
|
||||
let audioEnclosure = Enclosure(url: "test.mp3", type: "audio/mpeg")
|
||||
XCTAssertEqual(audioEnclosure.mimeType, .audio)
|
||||
|
||||
let videoEnclosure = Enclosure(url: "test.mp4", type: "video/mp4")
|
||||
XCTAssertEqual(videoEnclosure.mimeType, .video)
|
||||
|
||||
let articleEnclosure = Enclosure(url: "test.pdf", type: "application/pdf")
|
||||
XCTAssertEqual(articleEnclosure.mimeType, .article)
|
||||
}
|
||||
|
||||
func testFeedItemDebugDescription() {
|
||||
let item = FeedItem(
|
||||
id: "test-id",
|
||||
title: "Debug Test",
|
||||
author: "Test Author",
|
||||
published: Date(millisecondsSince1970: 1609459200000),
|
||||
subscriptionId: "sub-id",
|
||||
subscriptionTitle: "My Feed"
|
||||
)
|
||||
|
||||
let debugDesc = item.debugDescription
|
||||
XCTAssertTrue(debugDesc.contains("test-id"))
|
||||
XCTAssertTrue(debugDesc.contains("Debug Test"))
|
||||
XCTAssertTrue(debugDesc.contains("Test Author"))
|
||||
XCTAssertTrue(debugDesc.contains("My Feed"))
|
||||
}
|
||||
}
|
||||
211
iOS/RSSuperTests/FeedParserTests.swift
Normal file
211
iOS/RSSuperTests/FeedParserTests.swift
Normal file
@@ -0,0 +1,211 @@
|
||||
import XCTest
|
||||
@testable import RSSuper
|
||||
|
||||
final class FeedParserTests: XCTestCase {
|
||||
func testParsesRSS20Feed() throws {
|
||||
let parser = FeedParser()
|
||||
let result = try parser.parse(
|
||||
data: Data(rssSample.utf8),
|
||||
sourceURL: "https://example.com/rss.xml"
|
||||
)
|
||||
|
||||
XCTAssertEqual(result.feedType, .rss)
|
||||
XCTAssertEqual(result.feed.title, "Example Podcast")
|
||||
XCTAssertEqual(result.feed.subtitle, "Weekly iOS and Swift updates")
|
||||
XCTAssertEqual(result.feed.items.count, 2)
|
||||
XCTAssertEqual(result.feed.ttl, 60)
|
||||
|
||||
let firstItem = try XCTUnwrap(result.feed.items.first)
|
||||
XCTAssertEqual(firstItem.title, "Episode 1")
|
||||
XCTAssertEqual(firstItem.author, "Host Name")
|
||||
XCTAssertEqual(firstItem.guid, "episode-1")
|
||||
XCTAssertEqual(firstItem.categories, ["Swift"])
|
||||
XCTAssertEqual(firstItem.enclosure?.url, "https://example.com/audio/ep1.mp3")
|
||||
XCTAssertEqual(firstItem.enclosure?.type, "audio/mpeg")
|
||||
XCTAssertEqual(firstItem.enclosure?.length, 12345)
|
||||
XCTAssertEqual(firstItem.content, "<p>Full content for episode 1.</p>")
|
||||
}
|
||||
|
||||
func testParsesAtom10Feed() throws {
|
||||
let parser = FeedParser()
|
||||
let result = try parser.parse(
|
||||
data: Data(atomSample.utf8),
|
||||
sourceURL: "https://example.com/atom.xml"
|
||||
)
|
||||
|
||||
XCTAssertEqual(result.feedType, .atom)
|
||||
XCTAssertEqual(result.feed.title, "Example Atom Feed")
|
||||
XCTAssertEqual(result.feed.subtitle, "Recent engineering posts")
|
||||
XCTAssertEqual(result.feed.link, "https://example.com")
|
||||
XCTAssertEqual(result.feed.items.count, 2)
|
||||
|
||||
let firstItem = try XCTUnwrap(result.feed.items.first)
|
||||
XCTAssertEqual(firstItem.guid, "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a")
|
||||
XCTAssertEqual(firstItem.link, "https://example.com/posts/1")
|
||||
XCTAssertEqual(firstItem.author, "Jane Author")
|
||||
XCTAssertEqual(firstItem.enclosure?.url, "https://example.com/audio/post1.mp3")
|
||||
XCTAssertEqual(firstItem.enclosure?.type, "audio/mpeg")
|
||||
XCTAssertEqual(firstItem.enclosure?.length, 2048)
|
||||
}
|
||||
|
||||
func testHandlesITunesNamespace() throws {
|
||||
let parser = FeedParser()
|
||||
let result = try parser.parse(
|
||||
data: Data(rssWithITunesSample.utf8),
|
||||
sourceURL: "https://example.com/itunes.xml"
|
||||
)
|
||||
|
||||
XCTAssertEqual(result.feed.subtitle, "Podcast subtitle")
|
||||
XCTAssertEqual(result.feed.description, "Feed-level summary")
|
||||
|
||||
let item = try XCTUnwrap(result.feed.items.first)
|
||||
XCTAssertEqual(item.author, "Podcast Author")
|
||||
XCTAssertEqual(item.description, "Item-level summary")
|
||||
}
|
||||
|
||||
func testThrowsForMalformedXML() {
|
||||
let parser = FeedParser()
|
||||
XCTAssertThrowsError(
|
||||
try parser.parse(
|
||||
data: Data("<rss><channel><title>Broken".utf8),
|
||||
sourceURL: "https://example.com/broken.xml"
|
||||
)
|
||||
) { error in
|
||||
XCTAssertEqual(error as? FeedParsingError, .malformedXML)
|
||||
}
|
||||
}
|
||||
|
||||
func testParsesRealWorldStyleFeeds() throws {
|
||||
let parser = FeedParser()
|
||||
let rssResult = try parser.parse(
|
||||
data: Data(realWorldRSSSample.utf8),
|
||||
sourceURL: "https://feeds.example.com/news.xml"
|
||||
)
|
||||
XCTAssertEqual(rssResult.feedType, .rss)
|
||||
XCTAssertGreaterThan(rssResult.feed.items.count, 0)
|
||||
|
||||
let atomResult = try parser.parse(
|
||||
data: Data(realWorldAtomSample.utf8),
|
||||
sourceURL: "https://feeds.example.com/engineering.xml"
|
||||
)
|
||||
XCTAssertEqual(atomResult.feedType, .atom)
|
||||
XCTAssertGreaterThan(atomResult.feed.items.count, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private let rssSample = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
|
||||
<channel>
|
||||
<title>Example Podcast</title>
|
||||
<link>https://example.com</link>
|
||||
<description>A sample RSS feed</description>
|
||||
<language>en-us</language>
|
||||
<lastBuildDate>Mon, 30 Mar 2026 10:00:00 +0000</lastBuildDate>
|
||||
<generator>RSSuper Test Suite</generator>
|
||||
<ttl>60</ttl>
|
||||
<itunes:subtitle>Weekly iOS and Swift updates</itunes:subtitle>
|
||||
<item>
|
||||
<title>Episode 1</title>
|
||||
<link>https://example.com/episodes/1</link>
|
||||
<guid>episode-1</guid>
|
||||
<pubDate>Mon, 30 Mar 2026 09:00:00 +0000</pubDate>
|
||||
<category>Swift</category>
|
||||
<description>Episode 1 summary</description>
|
||||
<content:encoded><![CDATA[<p>Full content for episode 1.</p>]]></content:encoded>
|
||||
<itunes:author>Host Name</itunes:author>
|
||||
<enclosure url="https://example.com/audio/ep1.mp3" type="audio/mpeg" length="12345" />
|
||||
</item>
|
||||
<item>
|
||||
<title>Episode 2</title>
|
||||
<link>https://example.com/episodes/2</link>
|
||||
<guid>episode-2</guid>
|
||||
<pubDate>Mon, 30 Mar 2026 08:00:00 +0000</pubDate>
|
||||
<description>Episode 2 summary</description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
"""
|
||||
|
||||
private let atomSample = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>Example Atom Feed</title>
|
||||
<subtitle>Recent engineering posts</subtitle>
|
||||
<link href="https://example.com" rel="alternate" />
|
||||
<updated>2026-03-30T10:00:00Z</updated>
|
||||
<generator>Atom Test Generator</generator>
|
||||
<entry>
|
||||
<title>Post One</title>
|
||||
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
|
||||
<updated>2026-03-30T09:00:00Z</updated>
|
||||
<published>2026-03-30T08:59:00Z</published>
|
||||
<summary>First post summary</summary>
|
||||
<content type="html"><p>First post full content</p></content>
|
||||
<author><name>Jane Author</name></author>
|
||||
<link href="https://example.com/posts/1" rel="alternate" />
|
||||
<link href="https://example.com/audio/post1.mp3" rel="enclosure" type="audio/mpeg" length="2048" />
|
||||
</entry>
|
||||
<entry>
|
||||
<title>Post Two</title>
|
||||
<id>urn:uuid:7a9b2f0d-65b2-44a7-a2ad-d3c3ff7dd003</id>
|
||||
<updated>2026-03-30T08:00:00Z</updated>
|
||||
<summary>Second post summary</summary>
|
||||
<link href="https://example.com/posts/2" rel="alternate" />
|
||||
</entry>
|
||||
</feed>
|
||||
"""
|
||||
|
||||
private let rssWithITunesSample = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
|
||||
<channel>
|
||||
<title>iTunes Feed</title>
|
||||
<itunes:subtitle>Podcast subtitle</itunes:subtitle>
|
||||
<itunes:summary>Feed-level summary</itunes:summary>
|
||||
<item>
|
||||
<title>Episode</title>
|
||||
<itunes:author>Podcast Author</itunes:author>
|
||||
<itunes:summary>Item-level summary</itunes:summary>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
"""
|
||||
|
||||
private let realWorldRSSSample = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>Daily Tech News</title>
|
||||
<link>https://news.example.com</link>
|
||||
<description>Latest updates from the tech world</description>
|
||||
<lastBuildDate>Mon, 30 Mar 2026 12:00:00 +0000</lastBuildDate>
|
||||
<item>
|
||||
<title>Apple ships new SDK tools</title>
|
||||
<link>https://news.example.com/apple-sdk-tools</link>
|
||||
<guid>news-1</guid>
|
||||
<pubDate>Mon, 30 Mar 2026 11:00:00 +0000</pubDate>
|
||||
<description>Tooling improvements for mobile developers.</description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
"""
|
||||
|
||||
private let realWorldAtomSample = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>Engineering Blog</title>
|
||||
<updated>2026-03-30T12:00:00Z</updated>
|
||||
<link rel="alternate" href="https://engineering.example.com" />
|
||||
<entry>
|
||||
<title>Improving app startup performance</title>
|
||||
<id>tag:engineering.example.com,2026:post-1</id>
|
||||
<updated>2026-03-29T16:00:00Z</updated>
|
||||
<published>2026-03-29T15:00:00Z</published>
|
||||
<summary>How we reduced cold-start by 25%.</summary>
|
||||
<content>Detailed analysis of startup bottlenecks and fixes.</content>
|
||||
<author><name>Engineering Team</name></author>
|
||||
<link rel="alternate" href="https://engineering.example.com/posts/startup-performance" />
|
||||
</entry>
|
||||
</feed>
|
||||
"""
|
||||
116
iOS/RSSuperTests/FeedSubscriptionTests.swift
Normal file
116
iOS/RSSuperTests/FeedSubscriptionTests.swift
Normal file
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// FeedSubscriptionTests.swift
|
||||
// RSSuperTests
|
||||
//
|
||||
// Created by Mike Freno on 3/29/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import RSSuper
|
||||
|
||||
final class FeedSubscriptionTests: XCTestCase {
|
||||
|
||||
func testFeedSubscriptionEncodingDecoding() throws {
|
||||
let subscription = FeedSubscription(
|
||||
id: "sub-123",
|
||||
url: "https://example.com/feed.xml",
|
||||
title: "Example Feed",
|
||||
category: "Tech",
|
||||
enabled: true,
|
||||
fetchInterval: 120,
|
||||
createdAt: Date(timeIntervalSince1970: 1609459200),
|
||||
updatedAt: Date(timeIntervalSince1970: 1609545600),
|
||||
lastFetchedAt: Date(timeIntervalSince1970: 1609632000),
|
||||
nextFetchAt: Date(timeIntervalSince1970: 1609718400),
|
||||
error: nil,
|
||||
httpAuth: HttpAuth(username: "user", password: "pass")
|
||||
)
|
||||
|
||||
let data = try JSONEncoder().encode(subscription)
|
||||
let decoded = try JSONDecoder().decode(FeedSubscription.self, from: data)
|
||||
|
||||
XCTAssertEqual(decoded.id, subscription.id)
|
||||
XCTAssertEqual(decoded.url, subscription.url)
|
||||
XCTAssertEqual(decoded.title, subscription.title)
|
||||
XCTAssertEqual(decoded.category, subscription.category)
|
||||
XCTAssertEqual(decoded.enabled, subscription.enabled)
|
||||
XCTAssertEqual(decoded.fetchInterval, subscription.fetchInterval)
|
||||
XCTAssertEqual(decoded.createdAt, subscription.createdAt)
|
||||
XCTAssertEqual(decoded.updatedAt, subscription.updatedAt)
|
||||
XCTAssertEqual(decoded.lastFetchedAt, subscription.lastFetchedAt)
|
||||
XCTAssertEqual(decoded.nextFetchAt, subscription.nextFetchAt)
|
||||
XCTAssertEqual(decoded.httpAuth?.username, subscription.httpAuth?.username)
|
||||
XCTAssertEqual(decoded.httpAuth?.password, subscription.httpAuth?.password)
|
||||
}
|
||||
|
||||
func testFeedSubscriptionOptionalProperties() throws {
|
||||
let subscription = FeedSubscription(
|
||||
id: "minimal-sub",
|
||||
url: "https://example.com/minimal.xml",
|
||||
title: "Minimal Feed"
|
||||
)
|
||||
|
||||
XCTAssertNil(subscription.category)
|
||||
XCTAssertNil(subscription.lastFetchedAt)
|
||||
XCTAssertNil(subscription.nextFetchAt)
|
||||
XCTAssertNil(subscription.error)
|
||||
XCTAssertNil(subscription.httpAuth)
|
||||
|
||||
let data = try JSONEncoder().encode(subscription)
|
||||
let decoded = try JSONDecoder().decode(FeedSubscription.self, from: data)
|
||||
|
||||
XCTAssertEqual(decoded.id, subscription.id)
|
||||
XCTAssertEqual(decoded.enabled, true)
|
||||
XCTAssertEqual(decoded.fetchInterval, 60)
|
||||
}
|
||||
|
||||
func testFeedSubscriptionEquality() {
|
||||
let sub1 = FeedSubscription(
|
||||
id: "same-id",
|
||||
url: "https://example.com/feed.xml",
|
||||
title: "Same Title"
|
||||
)
|
||||
|
||||
let sub2 = FeedSubscription(
|
||||
id: "same-id",
|
||||
url: "https://example.com/feed.xml",
|
||||
title: "Same Title"
|
||||
)
|
||||
|
||||
let sub3 = FeedSubscription(
|
||||
id: "different-id",
|
||||
url: "https://example.com/other.xml",
|
||||
title: "Different Title"
|
||||
)
|
||||
|
||||
XCTAssertEqual(sub1, sub2)
|
||||
XCTAssertNotEqual(sub1, sub3)
|
||||
}
|
||||
|
||||
func testHttpAuthEncodingDecoding() throws {
|
||||
let auth = HttpAuth(username: "testuser", password: "testpass")
|
||||
|
||||
let data = try JSONEncoder().encode(auth)
|
||||
let decoded = try JSONDecoder().decode(HttpAuth.self, from: data)
|
||||
|
||||
XCTAssertEqual(decoded.username, auth.username)
|
||||
XCTAssertEqual(decoded.password, auth.password)
|
||||
}
|
||||
|
||||
func testFeedSubscriptionDebugDescription() {
|
||||
let subscription = FeedSubscription(
|
||||
id: "debug-id",
|
||||
url: "https://example.com/debug.xml",
|
||||
title: "Debug Feed",
|
||||
enabled: false,
|
||||
fetchInterval: 30,
|
||||
error: "Connection timeout"
|
||||
)
|
||||
|
||||
let debugDesc = subscription.debugDescription
|
||||
XCTAssertTrue(debugDesc.contains("debug-id"))
|
||||
XCTAssertTrue(debugDesc.contains("Debug Feed"))
|
||||
XCTAssertTrue(debugDesc.contains("30min"))
|
||||
XCTAssertTrue(debugDesc.contains("Connection timeout"))
|
||||
}
|
||||
}
|
||||
102
iOS/RSSuperTests/NotificationPreferencesTests.swift
Normal file
102
iOS/RSSuperTests/NotificationPreferencesTests.swift
Normal file
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// NotificationPreferencesTests.swift
|
||||
// RSSuperTests
|
||||
//
|
||||
// Created by Mike Freno on 3/29/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import RSSuper
|
||||
|
||||
final class NotificationPreferencesTests: XCTestCase {
|
||||
|
||||
func testNotificationPreferencesEncodingDecoding() throws {
|
||||
let prefs = NotificationPreferences(
|
||||
newArticles: true,
|
||||
episodeReleases: false,
|
||||
customAlerts: true,
|
||||
badgeCount: true,
|
||||
sound: false,
|
||||
vibration: true
|
||||
)
|
||||
|
||||
let data = try JSONEncoder().encode(prefs)
|
||||
let decoded = try JSONDecoder().decode(NotificationPreferences.self, from: data)
|
||||
|
||||
XCTAssertEqual(decoded.newArticles, prefs.newArticles)
|
||||
XCTAssertEqual(decoded.episodeReleases, prefs.episodeReleases)
|
||||
XCTAssertEqual(decoded.customAlerts, prefs.customAlerts)
|
||||
XCTAssertEqual(decoded.badgeCount, prefs.badgeCount)
|
||||
XCTAssertEqual(decoded.sound, prefs.sound)
|
||||
XCTAssertEqual(decoded.vibration, prefs.vibration)
|
||||
}
|
||||
|
||||
func testNotificationPreferencesDefaults() {
|
||||
let prefs = NotificationPreferences()
|
||||
|
||||
XCTAssertEqual(prefs.newArticles, true)
|
||||
XCTAssertEqual(prefs.episodeReleases, true)
|
||||
XCTAssertEqual(prefs.customAlerts, false)
|
||||
XCTAssertEqual(prefs.badgeCount, true)
|
||||
XCTAssertEqual(prefs.sound, true)
|
||||
XCTAssertEqual(prefs.vibration, true)
|
||||
}
|
||||
|
||||
func testNotificationPreferencesAllEnabled() {
|
||||
let allEnabled = NotificationPreferences(
|
||||
newArticles: true,
|
||||
episodeReleases: true,
|
||||
customAlerts: true,
|
||||
badgeCount: true,
|
||||
sound: true,
|
||||
vibration: true
|
||||
)
|
||||
|
||||
XCTAssertTrue(allEnabled.allEnabled)
|
||||
}
|
||||
|
||||
func testNotificationPreferencesAnyEnabled() {
|
||||
let noneEnabled = NotificationPreferences(
|
||||
newArticles: false,
|
||||
episodeReleases: false,
|
||||
customAlerts: false,
|
||||
badgeCount: false,
|
||||
sound: false,
|
||||
vibration: false
|
||||
)
|
||||
|
||||
XCTAssertFalse(noneEnabled.anyEnabled)
|
||||
|
||||
let someEnabled = NotificationPreferences(
|
||||
newArticles: true,
|
||||
episodeReleases: false,
|
||||
customAlerts: false,
|
||||
badgeCount: false,
|
||||
sound: false,
|
||||
vibration: false
|
||||
)
|
||||
|
||||
XCTAssertTrue(someEnabled.anyEnabled)
|
||||
}
|
||||
|
||||
func testNotificationPreferencesEquality() {
|
||||
let prefs1 = NotificationPreferences(newArticles: true, episodeReleases: false)
|
||||
let prefs2 = NotificationPreferences(newArticles: true, episodeReleases: false)
|
||||
let prefs3 = NotificationPreferences(newArticles: false, episodeReleases: true)
|
||||
|
||||
XCTAssertEqual(prefs1, prefs2)
|
||||
XCTAssertNotEqual(prefs1, prefs3)
|
||||
}
|
||||
|
||||
func testNotificationPreferencesDebugDescription() {
|
||||
let prefs = NotificationPreferences(
|
||||
newArticles: true,
|
||||
episodeReleases: false,
|
||||
customAlerts: true
|
||||
)
|
||||
|
||||
let debugDesc = prefs.debugDescription
|
||||
XCTAssertTrue(debugDesc.contains("true"))
|
||||
XCTAssertTrue(debugDesc.contains("false"))
|
||||
}
|
||||
}
|
||||
18
iOS/RSSuperTests/RSSuperTests.swift
Normal file
18
iOS/RSSuperTests/RSSuperTests.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// RSSuperTests.swift
|
||||
// RSSuperTests
|
||||
//
|
||||
// Created by Mike Freno on 3/29/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import RSSuper
|
||||
|
||||
final class RSSuperTests: XCTestCase {
|
||||
|
||||
func testPlaceholder() {
|
||||
// Placeholder test - all actual tests are in dedicated test files
|
||||
XCTAssertTrue(true)
|
||||
}
|
||||
}
|
||||
79
iOS/RSSuperTests/ReadingPreferencesTests.swift
Normal file
79
iOS/RSSuperTests/ReadingPreferencesTests.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// ReadingPreferencesTests.swift
|
||||
// RSSuperTests
|
||||
//
|
||||
// Created by Mike Freno on 3/29/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import RSSuper
|
||||
|
||||
final class ReadingPreferencesTests: XCTestCase {
|
||||
|
||||
func testReadingPreferencesEncodingDecoding() throws {
|
||||
let prefs = ReadingPreferences(
|
||||
fontSize: .large,
|
||||
lineHeight: .loose,
|
||||
showTableOfContents: true,
|
||||
showReadingTime: false,
|
||||
showAuthor: true,
|
||||
showDate: false
|
||||
)
|
||||
|
||||
let data = try JSONEncoder().encode(prefs)
|
||||
let decoded = try JSONDecoder().decode(ReadingPreferences.self, from: data)
|
||||
|
||||
XCTAssertEqual(decoded.fontSize, prefs.fontSize)
|
||||
XCTAssertEqual(decoded.lineHeight, prefs.lineHeight)
|
||||
XCTAssertEqual(decoded.showTableOfContents, prefs.showTableOfContents)
|
||||
XCTAssertEqual(decoded.showReadingTime, prefs.showReadingTime)
|
||||
XCTAssertEqual(decoded.showAuthor, prefs.showAuthor)
|
||||
XCTAssertEqual(decoded.showDate, prefs.showDate)
|
||||
}
|
||||
|
||||
func testReadingPreferencesDefaults() {
|
||||
let prefs = ReadingPreferences()
|
||||
|
||||
XCTAssertEqual(prefs.fontSize, .medium)
|
||||
XCTAssertEqual(prefs.lineHeight, .relaxed)
|
||||
XCTAssertEqual(prefs.showTableOfContents, false)
|
||||
XCTAssertEqual(prefs.showReadingTime, true)
|
||||
XCTAssertEqual(prefs.showAuthor, true)
|
||||
XCTAssertEqual(prefs.showDate, true)
|
||||
}
|
||||
|
||||
func testFontSizePointValue() {
|
||||
XCTAssertEqual(ReadingPreferences.FontSize.small.pointValue, 14)
|
||||
XCTAssertEqual(ReadingPreferences.FontSize.medium.pointValue, 16)
|
||||
XCTAssertEqual(ReadingPreferences.FontSize.large.pointValue, 18)
|
||||
XCTAssertEqual(ReadingPreferences.FontSize.xlarge.pointValue, 20)
|
||||
}
|
||||
|
||||
func testLineHeightMultiplier() {
|
||||
XCTAssertEqual(ReadingPreferences.LineHeight.normal.multiplier, 1.2)
|
||||
XCTAssertEqual(ReadingPreferences.LineHeight.relaxed.multiplier, 1.5)
|
||||
XCTAssertEqual(ReadingPreferences.LineHeight.loose.multiplier, 1.8)
|
||||
}
|
||||
|
||||
func testReadingPreferencesEquality() {
|
||||
let prefs1 = ReadingPreferences(fontSize: .large, lineHeight: .loose)
|
||||
let prefs2 = ReadingPreferences(fontSize: .large, lineHeight: .loose)
|
||||
let prefs3 = ReadingPreferences(fontSize: .medium, lineHeight: .normal)
|
||||
|
||||
XCTAssertEqual(prefs1, prefs2)
|
||||
XCTAssertNotEqual(prefs1, prefs3)
|
||||
}
|
||||
|
||||
func testReadingPreferencesDebugDescription() {
|
||||
let prefs = ReadingPreferences(
|
||||
fontSize: .xlarge,
|
||||
lineHeight: .normal,
|
||||
showTableOfContents: true
|
||||
)
|
||||
|
||||
let debugDesc = prefs.debugDescription
|
||||
XCTAssertTrue(debugDesc.contains("xlarge"))
|
||||
XCTAssertTrue(debugDesc.contains("normal"))
|
||||
XCTAssertTrue(debugDesc.contains("true"))
|
||||
}
|
||||
}
|
||||
96
iOS/RSSuperTests/SearchFiltersTests.swift
Normal file
96
iOS/RSSuperTests/SearchFiltersTests.swift
Normal file
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// SearchFiltersTests.swift
|
||||
// RSSuperTests
|
||||
//
|
||||
// Created by Mike Freno on 3/29/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import RSSuper
|
||||
|
||||
final class SearchFiltersTests: XCTestCase {
|
||||
|
||||
func testSearchFiltersEncodingDecoding() throws {
|
||||
let filters = SearchFilters(
|
||||
dateFrom: Date(timeIntervalSince1970: 1609459200),
|
||||
dateTo: Date(timeIntervalSince1970: 1609545600),
|
||||
feedIds: ["feed-1", "feed-2"],
|
||||
authors: ["Author 1", "Author 2"],
|
||||
contentType: .audio
|
||||
)
|
||||
|
||||
let data = try JSONEncoder().encode(filters)
|
||||
let decoded = try JSONDecoder().decode(SearchFilters.self, from: data)
|
||||
|
||||
XCTAssertEqual(decoded.dateFrom, filters.dateFrom)
|
||||
XCTAssertEqual(decoded.dateTo, filters.dateTo)
|
||||
XCTAssertEqual(decoded.feedIds, filters.feedIds)
|
||||
XCTAssertEqual(decoded.authors, filters.authors)
|
||||
XCTAssertEqual(decoded.contentType, filters.contentType)
|
||||
}
|
||||
|
||||
func testSearchFiltersEmpty() throws {
|
||||
let filters = SearchFilters()
|
||||
|
||||
XCTAssertNil(filters.dateFrom)
|
||||
XCTAssertNil(filters.dateTo)
|
||||
XCTAssertNil(filters.feedIds)
|
||||
XCTAssertNil(filters.authors)
|
||||
XCTAssertNil(filters.contentType)
|
||||
|
||||
let data = try JSONEncoder().encode(filters)
|
||||
let decoded = try JSONDecoder().decode(SearchFilters.self, from: data)
|
||||
|
||||
XCTAssertNil(decoded.dateFrom)
|
||||
XCTAssertNil(decoded.feedIds)
|
||||
}
|
||||
|
||||
func testSearchFiltersEquality() {
|
||||
let filters1 = SearchFilters(
|
||||
feedIds: ["feed-1"],
|
||||
authors: ["Author 1"]
|
||||
)
|
||||
|
||||
let filters2 = SearchFilters(
|
||||
feedIds: ["feed-1"],
|
||||
authors: ["Author 1"]
|
||||
)
|
||||
|
||||
let filters3 = SearchFilters(
|
||||
feedIds: ["feed-2"],
|
||||
authors: ["Author 2"]
|
||||
)
|
||||
|
||||
XCTAssertEqual(filters1, filters2)
|
||||
XCTAssertNotEqual(filters1, filters3)
|
||||
}
|
||||
|
||||
func testSearchSortOptionLocalizedDescription() {
|
||||
XCTAssertEqual(SearchSortOption.relevance.localizedDescription, "Relevance")
|
||||
XCTAssertEqual(SearchSortOption.dateDesc.localizedDescription, "Date (Newest First)")
|
||||
XCTAssertEqual(SearchSortOption.dateAsc.localizedDescription, "Date (Oldest First)")
|
||||
XCTAssertEqual(SearchSortOption.titleAsc.localizedDescription, "Title (A-Z)")
|
||||
XCTAssertEqual(SearchSortOption.titleDesc.localizedDescription, "Title (Z-A)")
|
||||
XCTAssertEqual(SearchSortOption.feedAsc.localizedDescription, "Feed (A-Z)")
|
||||
XCTAssertEqual(SearchSortOption.feedDesc.localizedDescription, "Feed (Z-A)")
|
||||
}
|
||||
|
||||
func testContentTypeLocalizedDescription() {
|
||||
XCTAssertEqual(ContentType.article.localizedDescription, "Article")
|
||||
XCTAssertEqual(ContentType.audio.localizedDescription, "Audio")
|
||||
XCTAssertEqual(ContentType.video.localizedDescription, "Video")
|
||||
}
|
||||
|
||||
func testSearchFiltersDebugDescription() {
|
||||
let filters = SearchFilters(
|
||||
dateFrom: Date(timeIntervalSince1970: 1609459200),
|
||||
feedIds: ["feed-1", "feed-2"],
|
||||
contentType: .video
|
||||
)
|
||||
|
||||
let debugDesc = filters.debugDescription
|
||||
XCTAssertTrue(debugDesc.contains("feed-1"))
|
||||
XCTAssertTrue(debugDesc.contains("feed-2"))
|
||||
XCTAssertTrue(debugDesc.contains("Video"))
|
||||
}
|
||||
}
|
||||
122
iOS/RSSuperTests/SearchHistoryStoreTests.swift
Normal file
122
iOS/RSSuperTests/SearchHistoryStoreTests.swift
Normal file
@@ -0,0 +1,122 @@
|
||||
import XCTest
|
||||
@testable import RSSuper
|
||||
|
||||
/// Unit tests for SearchHistoryStore
|
||||
final class SearchHistoryStoreTests: XCTestCase {
|
||||
|
||||
private var historyStore: SearchHistoryStore!
|
||||
private var databaseManager: DatabaseManager!
|
||||
|
||||
override func setUp() async throws {
|
||||
// Create in-memory database for testing
|
||||
databaseManager = try await DatabaseManager.inMemory()
|
||||
historyStore = SearchHistoryStore(databaseManager: databaseManager, maxHistoryCount: 10)
|
||||
try await historyStore.initialize()
|
||||
}
|
||||
|
||||
override func tearDown() async throws {
|
||||
historyStore = nil
|
||||
databaseManager = nil
|
||||
}
|
||||
|
||||
func testRecordSearch() async throws {
|
||||
try await historyStore.recordSearch("test query")
|
||||
|
||||
let exists = try await historyStore.queryExists("test query")
|
||||
XCTAssertTrue(exists)
|
||||
}
|
||||
|
||||
func testRecordSearchUpdatesExisting() async throws {
|
||||
try await historyStore.recordSearch("test query")
|
||||
let firstCount = try await historyStore.getTotalCount()
|
||||
|
||||
try await historyStore.recordSearch("test query")
|
||||
let secondCount = try await historyStore.getTotalCount()
|
||||
|
||||
XCTAssertEqual(firstCount, secondCount) // Should be same, updated not inserted
|
||||
}
|
||||
|
||||
func testGetRecentQueries() async throws {
|
||||
try await historyStore.recordSearch("query 1")
|
||||
try await historyStore.recordSearch("query 2")
|
||||
try await historyStore.recordSearch("query 3")
|
||||
|
||||
let queries = try await historyStore.getRecentQueries(limit: 2)
|
||||
|
||||
XCTAssertEqual(queries.count, 2)
|
||||
XCTAssertEqual(queries[0], "query 3") // Most recent first
|
||||
XCTAssertEqual(queries[1], "query 2")
|
||||
}
|
||||
|
||||
func testGetHistoryWithMetadata() async throws {
|
||||
try await historyStore.recordSearch("test query", resultCount: 42)
|
||||
|
||||
let entries = try await historyStore.getHistoryWithMetadata(limit: 10)
|
||||
|
||||
XCTAssertEqual(entries.count, 1)
|
||||
XCTAssertEqual(entries[0].query, "test query")
|
||||
XCTAssertEqual(entries[0].resultCount, 42)
|
||||
}
|
||||
|
||||
func testRemoveQuery() async throws {
|
||||
try await historyStore.recordSearch("to remove")
|
||||
XCTAssertTrue(try await historyStore.queryExists("to remove"))
|
||||
|
||||
try await historyStore.removeQuery("to remove")
|
||||
XCTAssertFalse(try await historyStore.queryExists("to remove"))
|
||||
}
|
||||
|
||||
func testClearHistory() async throws {
|
||||
try await historyStore.recordSearch("query 1")
|
||||
try await historyStore.recordSearch("query 2")
|
||||
|
||||
XCTAssertEqual(try await historyStore.getTotalCount(), 2)
|
||||
|
||||
try await historyStore.clearHistory()
|
||||
XCTAssertEqual(try await historyStore.getTotalCount(), 0)
|
||||
}
|
||||
|
||||
func testTrimHistory() async throws {
|
||||
// Insert more than maxHistoryCount
|
||||
for i in 1...15 {
|
||||
try await historyStore.recordSearch("query \(i)")
|
||||
}
|
||||
|
||||
let count = try await historyStore.getTotalCount()
|
||||
XCTAssertEqual(count, 10) // Should be trimmed to maxHistoryCount
|
||||
}
|
||||
|
||||
func testGetPopularQueries() async throws {
|
||||
// Record queries with different frequencies
|
||||
try await historyStore.recordSearch("popular")
|
||||
try await historyStore.recordSearch("popular")
|
||||
try await historyStore.recordSearch("popular")
|
||||
try await historyStore.recordSearch("less popular")
|
||||
try await historyStore.recordSearch("less popular")
|
||||
try await historyStore.recordSearch("once")
|
||||
|
||||
let popular = try await historyStore.getPopularQueries(limit: 10)
|
||||
|
||||
XCTAssertEqual(popular.count, 3)
|
||||
XCTAssertEqual(popular[0].query, "popular")
|
||||
XCTAssertEqual(popular[0].count, 3)
|
||||
}
|
||||
|
||||
func testGetTodaysQueries() async throws {
|
||||
try await historyStore.recordSearch("today query 1")
|
||||
try await historyStore.recordSearch("today query 2")
|
||||
|
||||
let todays = try await historyStore.getTodaysQueries()
|
||||
|
||||
XCTAssertTrue(todays.contains("today query 1"))
|
||||
XCTAssertTrue(todays.contains("today query 2"))
|
||||
}
|
||||
|
||||
func testEmptyQueryIgnored() async throws {
|
||||
try await historyStore.recordSearch("")
|
||||
try await historyStore.recordSearch(" ")
|
||||
|
||||
let count = try await historyStore.getTotalCount()
|
||||
XCTAssertEqual(count, 0)
|
||||
}
|
||||
}
|
||||
111
iOS/RSSuperTests/SearchQueryTests.swift
Normal file
111
iOS/RSSuperTests/SearchQueryTests.swift
Normal file
@@ -0,0 +1,111 @@
|
||||
import XCTest
|
||||
@testable import RSSuper
|
||||
|
||||
/// Unit tests for SearchQuery parsing and manipulation
|
||||
final class SearchQueryTests: XCTestCase {
|
||||
|
||||
func testEmptyQuery() {
|
||||
let query = SearchQuery(rawValue: "")
|
||||
|
||||
XCTAssertEqual(query.terms, [])
|
||||
XCTAssertEqual(query.rawText, "")
|
||||
XCTAssertEqual(query.sort, .relevance)
|
||||
XCTAssertFalse(query.fuzzy)
|
||||
}
|
||||
|
||||
func testSimpleQuery() {
|
||||
let query = SearchQuery(rawValue: "swift programming")
|
||||
|
||||
XCTAssertEqual(query.terms, ["swift", "programming"])
|
||||
XCTAssertEqual(query.rawText, "swift programming")
|
||||
}
|
||||
|
||||
func testQueryWithDateFilter() {
|
||||
let query = SearchQuery(rawValue: "swift date:after:2024-01-01")
|
||||
|
||||
XCTAssertEqual(query.terms, ["swift"])
|
||||
XCTAssertNotNil(query.filters.dateRange)
|
||||
|
||||
if case .after(let date) = query.filters.dateRange! {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
let expectedDate = formatter.date(from: "2024-01-01")!
|
||||
XCTAssertEqual(date, expectedDate)
|
||||
} else {
|
||||
XCTFail("Expected .after case")
|
||||
}
|
||||
}
|
||||
|
||||
func testQueryWithFeedFilter() {
|
||||
let query = SearchQuery(rawValue: "swift feed:Apple Developer")
|
||||
|
||||
XCTAssertEqual(query.terms, ["swift"])
|
||||
XCTAssertEqual(query.filters.feedTitle, "Apple Developer")
|
||||
}
|
||||
|
||||
func testQueryWithAuthorFilter() {
|
||||
let query = SearchQuery(rawValue: "swift author:John Doe")
|
||||
|
||||
XCTAssertEqual(query.terms, ["swift"])
|
||||
XCTAssertEqual(query.filters.author, "John Doe")
|
||||
}
|
||||
|
||||
func testQueryWithSortOption() {
|
||||
let query = SearchQuery(rawValue: "swift sort:date_desc")
|
||||
|
||||
XCTAssertEqual(query.terms, ["swift"])
|
||||
XCTAssertEqual(query.sort, .dateDesc)
|
||||
}
|
||||
|
||||
func testQueryWithFuzzyFlag() {
|
||||
let query = SearchQuery(rawValue: "swift ~")
|
||||
|
||||
XCTAssertEqual(query.terms, ["swift"])
|
||||
XCTAssertTrue(query.fuzzy)
|
||||
}
|
||||
|
||||
func testFTSQueryGeneration() {
|
||||
let exactQuery = SearchQuery(rawValue: "swift programming")
|
||||
XCTAssertEqual(exactQuery.ftsQuery(), "\"swift\" OR \"programming\"")
|
||||
|
||||
let fuzzyQuery = SearchQuery(rawValue: "swift ~")
|
||||
XCTAssertEqual(fuzzyQuery.ftsQuery(), "\"*swift*\"")
|
||||
}
|
||||
|
||||
func testDisplayString() {
|
||||
let query = SearchQuery(rawValue: "swift date:after:2024-01-01")
|
||||
XCTAssertEqual(query.displayString, "swift")
|
||||
}
|
||||
|
||||
func testDateRangeLowerBound() {
|
||||
let afterRange = DateRange.after(Date())
|
||||
XCTAssertNotNil(afterRange.lowerBound)
|
||||
XCTAssertNil(afterRange.upperBound)
|
||||
|
||||
let beforeRange = DateRange.before(Date())
|
||||
XCTAssertNil(beforeRange.lowerBound)
|
||||
XCTAssertNotNil(beforeRange.upperBound)
|
||||
|
||||
let exactRange = DateRange.exact(Date())
|
||||
XCTAssertNotNil(exactRange.lowerBound)
|
||||
XCTAssertNotNil(exactRange.upperBound)
|
||||
}
|
||||
|
||||
func testSearchFiltersIsEmpty() {
|
||||
var filters = SearchFilters()
|
||||
XCTAssertTrue(filters.isEmpty)
|
||||
|
||||
filters.dateRange = .after(Date())
|
||||
XCTAssertFalse(filters.isEmpty)
|
||||
|
||||
filters = .empty
|
||||
XCTAssertTrue(filters.isEmpty)
|
||||
}
|
||||
|
||||
func testSortOptionOrderByClause() {
|
||||
XCTAssertEqual(SearchSortOption.relevance.orderByClause(), "rank")
|
||||
XCTAssertEqual(SearchSortOption.dateDesc.orderByClause(), "f.published DESC")
|
||||
XCTAssertEqual(SearchSortOption.titleAsc.orderByClause(), "f.title ASC")
|
||||
XCTAssertEqual(SearchSortOption.feedDesc.orderByClause(), "s.title DESC")
|
||||
}
|
||||
}
|
||||
89
iOS/RSSuperTests/SearchResultTests.swift
Normal file
89
iOS/RSSuperTests/SearchResultTests.swift
Normal file
@@ -0,0 +1,89 @@
|
||||
import XCTest
|
||||
@testable import RSSuper
|
||||
|
||||
/// Unit tests for SearchResult and related types
|
||||
final class SearchResultTests: XCTestCase {
|
||||
|
||||
func testArticleResultCreation() {
|
||||
let result = SearchResult.article(
|
||||
id: "article-123",
|
||||
title: "Test Article",
|
||||
snippet: "This is a snippet",
|
||||
link: "https://example.com/article",
|
||||
feedTitle: "Test Feed",
|
||||
published: Date(),
|
||||
score: 0.95,
|
||||
author: "Test Author"
|
||||
)
|
||||
|
||||
XCTAssertEqual(result.id, "article-123")
|
||||
XCTAssertEqual(result.type, .article)
|
||||
XCTAssertEqual(result.title, "Test Article")
|
||||
XCTAssertEqual(result.snippet, "This is a snippet")
|
||||
XCTAssertEqual(result.link, "https://example.com/article")
|
||||
XCTAssertEqual(result.feedTitle, "Test Feed")
|
||||
XCTAssertEqual(result.score, 0.95)
|
||||
XCTAssertEqual(result.author, "Test Author")
|
||||
}
|
||||
|
||||
func testFeedResultCreation() {
|
||||
let result = SearchResult.feed(
|
||||
id: "feed-456",
|
||||
title: "Test Feed",
|
||||
link: "https://example.com/feed.xml",
|
||||
score: 0.85
|
||||
)
|
||||
|
||||
XCTAssertEqual(result.id, "feed-456")
|
||||
XCTAssertEqual(result.type, .feed)
|
||||
XCTAssertEqual(result.title, "Test Feed")
|
||||
XCTAssertEqual(result.link, "https://example.com/feed.xml")
|
||||
XCTAssertEqual(result.score, 0.85)
|
||||
}
|
||||
|
||||
func testSuggestionResultCreation() {
|
||||
let result = SearchResult.suggestion(
|
||||
text: "swift programming",
|
||||
score: 0.75
|
||||
)
|
||||
|
||||
XCTAssertEqual(result.type, .suggestion)
|
||||
XCTAssertEqual(result.title, "swift programming")
|
||||
XCTAssertEqual(result.score, 0.75)
|
||||
}
|
||||
|
||||
func testSearchResultTypeEncoding() {
|
||||
XCTAssertEqual(SearchResultType.article.rawValue, "article")
|
||||
XCTAssertEqual(SearchResultType.feed.rawValue, "feed")
|
||||
XCTAssertEqual(SearchResultType.suggestion.rawValue, "suggestion")
|
||||
XCTAssertEqual(SearchResultType.tag.rawValue, "tag")
|
||||
XCTAssertEqual(SearchResultType.author.rawValue, "author")
|
||||
}
|
||||
|
||||
func testSearchResultEquatable() {
|
||||
let result1 = SearchResult.article(id: "1", title: "Test")
|
||||
let result2 = SearchResult.article(id: "1", title: "Test")
|
||||
let result3 = SearchResult.article(id: "2", title: "Test")
|
||||
|
||||
XCTAssertEqual(result1, result2)
|
||||
XCTAssertNotEqual(result1, result3)
|
||||
}
|
||||
|
||||
func testSearchResults totalCount() {
|
||||
let results = SearchResults(
|
||||
articles: [SearchResult.article(id: "1", title: "A")],
|
||||
feeds: [SearchResult.feed(id: "2", title: "F")],
|
||||
suggestions: []
|
||||
)
|
||||
|
||||
XCTAssertEqual(results.totalCount, 2)
|
||||
XCTAssertTrue(results.hasResults)
|
||||
}
|
||||
|
||||
func testSearchResultsEmpty() {
|
||||
let results = SearchResults(articles: [], feeds: [], suggestions: [])
|
||||
|
||||
XCTAssertEqual(results.totalCount, 0)
|
||||
XCTAssertFalse(results.hasResults)
|
||||
}
|
||||
}
|
||||
76
iOS/RSSuperTests/SyncSchedulerTests.swift
Normal file
76
iOS/RSSuperTests/SyncSchedulerTests.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
import XCTest
|
||||
@testable import RSSuper
|
||||
|
||||
/// Unit tests for SyncScheduler
|
||||
final class SyncSchedulerTests: XCTestCase {
|
||||
|
||||
private var scheduler: SyncScheduler!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
scheduler = SyncScheduler()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
scheduler = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testDefaultSyncInterval() {
|
||||
XCTAssertEqual(scheduler.preferredSyncInterval, SyncScheduler.defaultSyncInterval)
|
||||
}
|
||||
|
||||
func testSyncIntervalClamping() {
|
||||
// Test minimum clamping
|
||||
scheduler.preferredSyncInterval = 60 // 1 minute
|
||||
XCTAssertEqual(scheduler.preferredSyncInterval, SyncScheduler.minimumSyncInterval)
|
||||
|
||||
// Test maximum clamping
|
||||
scheduler.preferredSyncInterval = 48 * 3600 // 48 hours
|
||||
XCTAssertEqual(scheduler.preferredSyncInterval, SyncScheduler.maximumSyncInterval)
|
||||
}
|
||||
|
||||
func testIsSyncDue() {
|
||||
// Fresh scheduler should have sync due
|
||||
XCTAssertTrue(scheduler.isSyncDue)
|
||||
|
||||
// Set last sync date to recent past
|
||||
scheduler.lastSyncDate = Date().addingTimeInterval(-1 * 3600) // 1 hour ago
|
||||
XCTAssertFalse(scheduler.isSyncDue)
|
||||
|
||||
// Set last sync date to far past
|
||||
scheduler.lastSyncDate = Date().addingTimeInterval(-12 * 3600) // 12 hours ago
|
||||
XCTAssertTrue(scheduler.isSyncDue)
|
||||
}
|
||||
|
||||
func testTimeSinceLastSync() {
|
||||
scheduler.lastSyncDate = Date().addingTimeInterval(-3600) // 1 hour ago
|
||||
|
||||
let timeSince = scheduler.timeSinceLastSync
|
||||
XCTAssertGreaterThan(timeSince, 3500)
|
||||
XCTAssertLessThan(timeSince, 3700)
|
||||
}
|
||||
|
||||
func testResetSyncSchedule() {
|
||||
scheduler.preferredSyncInterval = 12 * 3600
|
||||
scheduler.lastSyncDate = Date().addingTimeInterval(-100)
|
||||
|
||||
scheduler.resetSyncSchedule()
|
||||
|
||||
XCTAssertEqual(scheduler.preferredSyncInterval, SyncScheduler.defaultSyncInterval)
|
||||
XCTAssertNil(scheduler.lastSyncDate)
|
||||
}
|
||||
|
||||
func testUserActivityLevelCalculation() {
|
||||
// High activity
|
||||
XCTAssertEqual(UserActivityLevel.calculate(from: 5, lastOpenedAgo: 3600), .high)
|
||||
XCTAssertEqual(UserActivityLevel.calculate(from: 1, lastOpenedAgo: 60), .high)
|
||||
|
||||
// Medium activity
|
||||
XCTAssertEqual(UserActivityLevel.calculate(from: 2, lastOpenedAgo: 3600), .medium)
|
||||
XCTAssertEqual(UserActivityLevel.calculate(from: 0, lastOpenedAgo: 43200), .medium)
|
||||
|
||||
// Low activity
|
||||
XCTAssertEqual(UserActivityLevel.calculate(from: 0, lastOpenedAgo: 172800), .low)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user