Files
andros 15eb8acbd0 Network performance: ETag caching, partial Range fetches, progressive timeline streaming
- FeedCache actor: conditional GET with ETag/Last-Modified, returns 304 hits from cache
- PartialFeedFetcher: Range requests for header-only Discover fetches and date-filtered timeline fetches (2 requests instead of 3 per feed)
- TimelineFetcher: AsyncStream-based streaming emits posts as each feed arrives instead of waiting for all feeds
- TimelineViewModel: consumes stream progressively so posts appear immediately on first feed
- Discover uses header-only fetch (16 KB vs full file)
- maxConcurrentDownloads raised from 10 to 20
2026-05-16 10:12:41 +02:00

354 lines
14 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import Testing
@testable import OrgSocialKit
// Well-known feeds registered on the public relay (relay.org-social.org/feeds/).
// All three are active and have many historical posts ideal for testing
// Range-based partial downloads vs. full downloads.
private let shomURL = URL(string: "https://shom.dev/social.org")!
private let rossURL = URL(string: "https://rossabaker.com/social.org")!
private let sachaURL = URL(string: "https://sachachua.com/social.org")!
private let testFeeds = [shomURL, rossURL, sachaURL]
private let relayURL = URL(string: "https://relay.org-social.org")!
// MARK: - Feature 2: Header-only fetch
@Suite("PartialFeedFetcher header-only fetch (Discover)", .serialized)
struct PartialFeedFetcherHeaderTests {
let partial = PartialFeedFetcher()
let full = ProfileFetcher()
// --- Correctness: metadata matches full fetch ---
@Test("fetchProfileHeader nick matches full profile for shom.dev")
func headerNickMatchesFullShom() async throws {
async let partialProfile = partial.fetchProfileHeader(from: shomURL)
async let fullProfile = try full.fetch(from: shomURL)
let p = await partialProfile
let f = try await fullProfile
#expect(p != nil, "fetchProfileHeader must not return nil")
#expect(p?.nick == f.nick, "nick must match full profile")
}
@Test("fetchProfileHeader title matches full profile for rossabaker.com")
func headerTitleMatchesFullRoss() async throws {
async let partialProfile = partial.fetchProfileHeader(from: rossURL)
async let fullProfile = try full.fetch(from: rossURL)
let p = await partialProfile
let f = try await fullProfile
#expect(p != nil)
#expect(p?.title == f.title, "title must match full profile")
}
@Test("fetchProfileHeader avatar matches full profile for sachachua.com")
func headerAvatarMatchesFullSacha() async throws {
async let partialProfile = partial.fetchProfileHeader(from: sachaURL)
async let fullProfile = try full.fetch(from: sachaURL)
let p = await partialProfile
let f = try await fullProfile
#expect(p != nil)
#expect(p?.avatar == f.avatar, "avatar URL must match full profile")
}
@Test("fetchProfileHeader description matches full profile for all test feeds")
func headerDescriptionMatchesFull() async throws {
for url in testFeeds {
async let partialProfile = partial.fetchProfileHeader(from: url)
async let fullProfile = try full.fetch(from: url)
let p = await partialProfile
let f = try await fullProfile
#expect(p?.description == f.description, "description mismatch for \(url)")
}
}
// --- Structural guarantees ---
@Test("fetchProfileHeader sets feedURL to the requested URL")
func headerSetsFeedURL() async {
for url in testFeeds {
let p = await partial.fetchProfileHeader(from: url)
#expect(p?.feedURL == url, "feedURL must be set for \(url)")
}
}
@Test("fetchProfileHeader always returns zero posts")
func headerAlwaysReturnsZeroPosts() async {
// Posts section is trimmed before parsing, so no posts should ever appear.
for url in testFeeds {
let p = await partial.fetchProfileHeader(from: url)
#expect(p != nil, "fetchProfileHeader must not return nil for \(url)")
#expect(p?.posts.isEmpty == true,
"header fetch must not include posts for \(url)")
}
}
@Test("fetchProfileHeader nick and feedURL are non-nil for all test feeds")
func headerHasIdentity() async {
for url in testFeeds {
let p = await partial.fetchProfileHeader(from: url)
#expect(p?.feedURL == url)
#expect(p?.nick != nil, "nick must be present for \(url)")
}
}
// --- Input validation ---
@Test("fetchProfileHeader returns nil for ftp:// scheme")
func headerInvalidSchemeReturnsNil() async {
let url = URL(string: "ftp://bad.example.com/social.org")!
let result = await partial.fetchProfileHeader(from: url)
#expect(result == nil)
}
@Test("fetchProfileHeader returns nil for non-existent host")
func headerNonExistentHostReturnsNil() async {
let url = URL(string: "https://this-host-does-not-exist.invalid/social.org")!
let result = await partial.fetchProfileHeader(from: url)
#expect(result == nil)
}
// --- Relay integration ---
@Test("fetchProfileHeader succeeds for feeds sampled from the real relay")
func headerSucceedsForRelayFeeds() async throws {
let client = RelayClient()
let allFeeds = try await client.fetchFeeds(from: relayURL)
#expect(!allFeeds.isEmpty, "Relay must return at least one feed")
// Sample 5 feeds deterministically (first five from the relay list).
let sampled = Array(allFeeds.prefix(5))
var successCount = 0
for url in sampled {
if let p = await partial.fetchProfileHeader(from: url),
p.nick != nil || p.title != nil {
successCount += 1
}
}
// Tolerate up to 2 offline feeds out of 5.
#expect(successCount >= 3,
"At least 3/5 relay feeds must return valid header metadata; got \(successCount)/\(sampled.count)")
}
}
// MARK: - Feature 3: Date-filtered fetch (Timeline)
@Suite("PartialFeedFetcher date-filtered fetch (Timeline)", .serialized)
struct PartialFeedFetcherSinceTests {
let partial = PartialFeedFetcher()
let full = ProfileFetcher()
let parser = OrgSocialParser()
private func cutoff(daysAgo: Int) -> Date {
Calendar.current.date(byAdding: .day, value: -daysAgo, to: Date())!
}
// --- Date correctness ---
@Test("fetchSince: every returned post is newer than the cutoff (shom.dev, 30 days)")
func sinceAllPostsWithinRange_Shom() async throws {
let since = cutoff(daysAgo: 30)
let content = await partial.fetchSince(from: shomURL, since: since)
#expect(content != nil, "fetchSince must not return nil for shom.dev")
let profile = parser.parse(content!)
for post in profile.posts {
#expect(post.date >= since,
"Post \(post.timestamp) is older than cutoff \(since)")
}
}
@Test("fetchSince: every returned post is newer than the cutoff (rossabaker, 60 days)")
func sinceAllPostsWithinRange_Ross() async throws {
let since = cutoff(daysAgo: 60)
let content = await partial.fetchSince(from: rossURL, since: since)
#expect(content != nil)
let profile = parser.parse(content!)
for post in profile.posts {
#expect(post.date >= since,
"Post \(post.timestamp) is older than cutoff \(since)")
}
}
@Test("fetchSince: every returned post is newer than the cutoff (sachachua, 14 days)")
func sinceAllPostsWithinRange_Sacha() async throws {
let since = cutoff(daysAgo: 14)
let content = await partial.fetchSince(from: sachaURL, since: since)
#expect(content != nil)
let profile = parser.parse(content!)
for post in profile.posts {
#expect(post.date >= since,
"Post \(post.timestamp) is older than cutoff \(since)")
}
}
// --- Count comparison: partial full ---
@Test("fetchSince post count is <= full fetch post count (shom.dev)")
func sincePostCountNotExceedsFull_Shom() async throws {
let since = cutoff(daysAgo: 30)
async let partialContent = partial.fetchSince(from: shomURL, since: since)
async let fullProfile = try full.fetch(from: shomURL)
let p = await partialContent
let f = try await fullProfile
#expect(p != nil)
let partialCount = parser.parse(p!).posts.count
#expect(partialCount <= f.posts.count,
"Partial fetch returned more posts (\(partialCount)) than full fetch (\(f.posts.count))")
}
@Test("fetchSince post count is <= full fetch post count (rossabaker.com)")
func sincePostCountNotExceedsFull_Ross() async throws {
let since = cutoff(daysAgo: 60)
async let partialContent = partial.fetchSince(from: rossURL, since: since)
async let fullProfile = try full.fetch(from: rossURL)
let p = await partialContent
let f = try await fullProfile
#expect(p != nil)
let partialCount = parser.parse(p!).posts.count
#expect(partialCount <= f.posts.count,
"Partial fetch returned more posts (\(partialCount)) than full fetch (\(f.posts.count))")
}
// --- Metadata consistency ---
@Test("fetchSince: profile metadata (nick/title/avatar) matches full fetch")
func sinceMetadataMatchesFull() async throws {
let since = cutoff(daysAgo: 30)
for url in testFeeds {
async let partialContent = partial.fetchSince(from: url, since: since)
async let fullProfile = try full.fetch(from: url)
let p = await partialContent
let f = try await fullProfile
#expect(p != nil, "fetchSince returned nil for \(url)")
let partialProfile = parser.parse(p!)
#expect(partialProfile.nick == f.nick,
"nick mismatch for \(url): '\(partialProfile.nick ?? "nil")' vs '\(f.nick ?? "nil")'")
#expect(partialProfile.title == f.title,
"title mismatch for \(url)")
#expect(partialProfile.avatar == f.avatar,
"avatar mismatch for \(url)")
}
}
// --- Posts from partial match posts from full (same timestamps) ---
@Test("fetchSince: returned posts are a subset of the full fetch posts")
func sincePostsAreSubsetOfFull() async throws {
let since = cutoff(daysAgo: 30)
async let partialContent = partial.fetchSince(from: shomURL, since: since)
async let fullProfile = try full.fetch(from: shomURL)
let p = await partialContent
let f = try await fullProfile
#expect(p != nil)
let partialPosts = parser.parse(p!).posts
let fullTimestamps = Set(f.posts.map(\.timestamp))
for post in partialPosts {
#expect(fullTimestamps.contains(post.timestamp),
"Partial post \(post.timestamp) not found in full fetch")
}
}
// --- Input validation ---
@Test("fetchSince returns nil for ftp:// scheme")
func sinceInvalidSchemeReturnsNil() async {
let url = URL(string: "ftp://bad.example.com/social.org")!
let result = await partial.fetchSince(from: url, since: cutoff(daysAgo: 14))
#expect(result == nil)
}
@Test("fetchSince returns nil for non-existent host")
func sinceNonExistentHostReturnsNil() async {
let url = URL(string: "https://this-host-does-not-exist.invalid/social.org")!
let result = await partial.fetchSince(from: url, since: cutoff(daysAgo: 14))
#expect(result == nil)
}
// --- Relay integration ---
@Test("fetchSince succeeds and produces valid org text for all test feeds")
func sinceSucceedsForAllTestFeeds() async throws {
let since = cutoff(daysAgo: 14)
for url in testFeeds {
let content = await partial.fetchSince(from: url, since: since)
#expect(content != nil, "fetchSince returned nil for \(url)")
let profile = parser.parse(content!)
// Header metadata must survive partial reconstruction.
#expect(profile.nick != nil || profile.title != nil,
"Partial fetch for \(url) must contain profile metadata")
// Every post in the result must be within the requested range.
for post in profile.posts {
#expect(post.date >= since,
"Post \(post.timestamp) from \(url) is outside the requested range")
}
}
}
@Test("fetchSince with far-future cutoff returns empty posts but valid header")
func sinceFutureCutoffReturnsEmptyPosts() async throws {
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date())!
let content = await partial.fetchSince(from: shomURL, since: tomorrow)
#expect(content != nil, "fetchSince must not return nil even when no posts match")
let profile = parser.parse(content!)
#expect(profile.nick != nil, "Header metadata must still be present")
#expect(profile.posts.isEmpty, "No posts should be returned for a future cutoff")
}
@Test("fetchSince with very old cutoff returns at least some posts")
func sinceOldCutoffReturnsSomePosts() async throws {
// 10 years ago every feed should have posts newer than this.
let tenYearsAgo = Calendar.current.date(byAdding: .year, value: -10, to: Date())!
let content = await partial.fetchSince(from: shomURL, since: tenYearsAgo)
#expect(content != nil)
let profile = parser.parse(content!)
#expect(!profile.posts.isEmpty,
"At least some posts should be returned for a very old cutoff")
}
@Test("fetchSince relay sample: at least 3 of 5 feeds return valid partial content")
func sinceSucceedsForRelayFeedSample() async throws {
let client = RelayClient()
let allFeeds = try await client.fetchFeeds(from: relayURL)
#expect(!allFeeds.isEmpty)
let since = cutoff(daysAgo: 30)
let sampled = Array(allFeeds.prefix(5))
var successCount = 0
for url in sampled {
if let content = await partial.fetchSince(from: url, since: since) {
let profile = parser.parse(content)
if profile.nick != nil || profile.title != nil {
successCount += 1
}
}
}
#expect(successCount >= 3,
"At least 3/5 relay feeds must return valid partial content; got \(successCount)/\(sampled.count)")
}
}