379 lines
12 KiB
Swift
379 lines
12 KiB
Swift
import ManaCore
|
|
import XCTest
|
|
@testable import PagetaNative
|
|
|
|
final class PagetaNativeTests: XCTestCase {
|
|
// MARK: - AppConfig
|
|
|
|
func testAppConfig_apiPointsToPagetaDotCom() {
|
|
XCTAssertEqual(AppConfig.apiBaseURL.absoluteString, "https://api.pageta.com")
|
|
}
|
|
|
|
func testAppConfig_authPointsToManaAuth() {
|
|
XCTAssertEqual(AppConfig.manaAppConfig.authBaseURL.absoluteString, "https://auth.mana.how")
|
|
}
|
|
|
|
func testAppConfig_keychainService_usesSharedManaGroup() {
|
|
// Cross-App-SSO: alle mana-Apps schreiben Tokens unter der
|
|
// gleichen Keychain-Service-Bezeichnung. Login in einer App
|
|
// macht alle anderen sofort eingeloggt (sofern Keychain-Sharing-
|
|
// Entitlement matched).
|
|
XCTAssertEqual(AppConfig.manaAppConfig.keychainService, ManaSharedKeychainGroup)
|
|
}
|
|
|
|
func testAppConfig_keychainAccessGroup_usesSharedManaGroup() {
|
|
XCTAssertEqual(AppConfig.manaAppConfig.keychainAccessGroup, ManaSharedKeychainGroup)
|
|
}
|
|
|
|
func testAppConfig_appGroup_isPagetaSpecific() {
|
|
// App-Group bleibt pageta-eigen (für Widget/ShareExt-Daten-
|
|
// Sharing), nur die Keychain ist cross-app.
|
|
XCTAssertEqual(AppConfig.manaAppConfig.appGroup, "group.ev.mana.pageta")
|
|
}
|
|
|
|
// MARK: - Topic
|
|
|
|
func testTopic_allCases_haveLabelsAndEmoji() {
|
|
for topic in Topic.allCases {
|
|
XCTAssertFalse(topic.label.isEmpty, "Topic \(topic.rawValue) has empty label")
|
|
XCTAssertFalse(topic.emoji.isEmpty, "Topic \(topic.rawValue) has empty emoji")
|
|
}
|
|
}
|
|
|
|
func testTopic_matchesWireFormat() throws {
|
|
// Mirror der ALL_TOPICS-Liste aus shared-rss/types.ts. Driftet
|
|
// die Liste auseinander, brechen Pool-Responses.
|
|
let expected: Set<String> = [
|
|
"tech", "wissenschaft", "weltgeschehen", "wirtschaft",
|
|
"kultur", "gesundheit", "politik",
|
|
]
|
|
let actual = Set(Topic.allCases.map(\.rawValue))
|
|
XCTAssertEqual(actual, expected)
|
|
}
|
|
|
|
// MARK: - DTO-Decoding
|
|
|
|
func testFeedItem_decodesFromWireFormat() throws {
|
|
let json = """
|
|
{
|
|
"id": "abc-123",
|
|
"originalUrl": "https://example.com/a",
|
|
"title": "Beispiel-Artikel",
|
|
"excerpt": "Kurzer Auszug",
|
|
"content": "Voller Text",
|
|
"author": "Autor X",
|
|
"siteName": "Example",
|
|
"sourceSlug": "example",
|
|
"imageUrl": null,
|
|
"topic": "tech",
|
|
"language": "de",
|
|
"wordCount": 500,
|
|
"readingTimeMinutes": 3,
|
|
"publishedAt": "2026-05-18T10:00:00Z",
|
|
"ingestedAt": "2026-05-18T11:00:00Z",
|
|
"score": 1.23
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let item = try JSONDecoder().decode(FeedItem.self, from: json)
|
|
XCTAssertEqual(item.id, "abc-123")
|
|
XCTAssertEqual(item.topic, .tech)
|
|
XCTAssertEqual(item.score, 1.23)
|
|
XCTAssertNil(item.imageUrl)
|
|
}
|
|
|
|
func testArticle_decodesFromWireFormat() throws {
|
|
let json = """
|
|
{
|
|
"id": "art-1",
|
|
"userId": "user-42",
|
|
"originalUrl": "https://example.com/a",
|
|
"title": "Save-it-later-Item",
|
|
"excerpt": null,
|
|
"content": "...",
|
|
"htmlContent": null,
|
|
"author": null,
|
|
"siteName": "Example",
|
|
"imageUrl": null,
|
|
"wordCount": null,
|
|
"readingTimeMinutes": null,
|
|
"publishedAt": null,
|
|
"status": "unread",
|
|
"readingProgress": 0,
|
|
"isFavorite": false,
|
|
"savedAt": "2026-05-18T11:00:00Z",
|
|
"readAt": null,
|
|
"userNote": null,
|
|
"tags": [],
|
|
"extractedVersion": 1,
|
|
"createdAt": "2026-05-18T11:00:00Z",
|
|
"updatedAt": "2026-05-18T11:00:00Z"
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let article = try JSONDecoder().decode(Article.self, from: json)
|
|
XCTAssertEqual(article.id, "art-1")
|
|
XCTAssertEqual(article.status, .unread)
|
|
XCTAssertEqual(article.readingProgress, 0)
|
|
XCTAssertFalse(article.isFavorite)
|
|
}
|
|
|
|
func testArticleStatus_allCases_haveLabels() {
|
|
for status in ArticleStatus.allCases {
|
|
XCTAssertFalse(status.label.isEmpty)
|
|
}
|
|
}
|
|
|
|
// MARK: - URL-Routing
|
|
|
|
func testRoute_parsesPagetaDotComArticle() {
|
|
let url = URL(string: "https://pageta.com/article/abc-123")!
|
|
XCTAssertEqual(PagetaRoute.parse(url), .article(id: "abc-123"))
|
|
}
|
|
|
|
func testRoute_parsesPagetaSchemeArticle() {
|
|
let url = URL(string: "pageta://article/abc-123")!
|
|
XCTAssertEqual(PagetaRoute.parse(url), .article(id: "abc-123"))
|
|
}
|
|
|
|
func testRoute_caseInsensitiveHost() {
|
|
let url = URL(string: "HTTPS://PAGETA.COM/article/abc-123")!
|
|
XCTAssertEqual(PagetaRoute.parse(url), .article(id: "abc-123"))
|
|
}
|
|
|
|
func testRoute_rejectsOldManaHowDomain() {
|
|
// pageta.com ist Lead-Domain seit 2026-05-18 — die mana.how-
|
|
// Subdomain ist bewusst nicht im AASA für Native.
|
|
let url = URL(string: "https://pageta.mana.how/article/abc")!
|
|
XCTAssertNil(PagetaRoute.parse(url))
|
|
}
|
|
|
|
func testRoute_rejectsForeignHost() {
|
|
let url = URL(string: "https://example.com/article/abc")!
|
|
XCTAssertNil(PagetaRoute.parse(url))
|
|
}
|
|
|
|
func testRoute_rejectsEmptyArticleId() {
|
|
let url = URL(string: "https://pageta.com/article/")!
|
|
XCTAssertNil(PagetaRoute.parse(url))
|
|
}
|
|
|
|
func testRoute_rejectsUnknownPath() {
|
|
let url = URL(string: "https://pageta.com/about")!
|
|
XCTAssertNil(PagetaRoute.parse(url))
|
|
}
|
|
|
|
// MARK: - PendingShareStore (App-Group-Queue)
|
|
|
|
func testShareStore_enqueueAndDrain() {
|
|
let store = makeIsolatedStore()
|
|
XCTAssertTrue(store.currentQueue().isEmpty)
|
|
let url = URL(string: "https://example.com/a")!
|
|
XCTAssertTrue(store.enqueue(url))
|
|
XCTAssertEqual(store.currentQueue().map(\.absoluteString), [url.absoluteString])
|
|
}
|
|
|
|
func testShareStore_dedupesDuplicateEnqueue() {
|
|
let store = makeIsolatedStore()
|
|
let url = URL(string: "https://example.com/a")!
|
|
XCTAssertTrue(store.enqueue(url))
|
|
XCTAssertFalse(store.enqueue(url))
|
|
XCTAssertEqual(store.currentQueue().count, 1)
|
|
}
|
|
|
|
func testShareStore_removeAfterFlush() {
|
|
let store = makeIsolatedStore()
|
|
let a = URL(string: "https://example.com/a")!
|
|
let b = URL(string: "https://example.com/b")!
|
|
store.enqueue(a)
|
|
store.enqueue(b)
|
|
store.remove(a)
|
|
XCTAssertEqual(store.currentQueue().map(\.absoluteString), [b.absoluteString])
|
|
}
|
|
|
|
// MARK: - SharedHTMLStore (App-Group HTML-Cache für Consent-Wall-Bypass)
|
|
|
|
func testHTMLStore_roundTripKeyedByURL() {
|
|
let store = makeIsolatedHTMLStore()
|
|
let a = URL(string: "https://example.com/a")!
|
|
let b = URL(string: "https://example.com/b")!
|
|
XCTAssertNil(store.peek(for: a))
|
|
store.put("<html>A</html>", for: a)
|
|
XCTAssertEqual(store.peek(for: a), "<html>A</html>")
|
|
XCTAssertNil(store.peek(for: b), "Keys dürfen sich nicht überschneiden")
|
|
store.remove(for: a)
|
|
XCTAssertNil(store.peek(for: a), "remove muss den Blob löschen")
|
|
}
|
|
|
|
func testHTMLStore_dropsOversizeHTML() {
|
|
let store = makeIsolatedHTMLStore()
|
|
let url = URL(string: "https://example.com/big")!
|
|
let huge = String(repeating: "x", count: SharedHTMLStore.maxHTMLChars + 1)
|
|
store.put(huge, for: url)
|
|
XCTAssertNil(store.peek(for: url), "Zu großes HTML wird verworfen (API-Cap)")
|
|
}
|
|
|
|
func testHTMLStore_pruneRemovesStaleBlobs() {
|
|
let store = makeIsolatedHTMLStore(maxAgeDays: 0)
|
|
let url = URL(string: "https://example.com/old")!
|
|
store.put("<html></html>", for: url)
|
|
store.prune()
|
|
XCTAssertNil(store.peek(for: url), "maxAge 0 → alles gilt als verwaist")
|
|
}
|
|
|
|
private func makeIsolatedHTMLStore(maxAgeDays: Double = 7) -> SharedHTMLStore {
|
|
let dir = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("pageta-html-tests-\(UUID().uuidString)", isDirectory: true)
|
|
return SharedHTMLStore(directory: dir, maxAgeDays: maxAgeDays)
|
|
}
|
|
|
|
private func makeIsolatedStore() -> PendingShareStore {
|
|
// Test-isolierter Slot — kollidiert nicht mit dem prod-Key.
|
|
let key = "pageta.tests.shares.\(UUID().uuidString)"
|
|
let store = PendingShareStore(appGroup: "test.\(UUID().uuidString)", key: key)
|
|
store.clear()
|
|
return store
|
|
}
|
|
|
|
|
|
// MARK: - SpotlightIndex
|
|
|
|
#if canImport(CoreSpotlight)
|
|
func testSpotlight_searchableItem_carriesArticleFields() {
|
|
let article = Article(
|
|
id: "abc-123",
|
|
userId: "u",
|
|
originalUrl: "https://x/a",
|
|
title: "Beispiel-Artikel",
|
|
excerpt: "Kurzer Auszug",
|
|
content: "Body",
|
|
htmlContent: nil,
|
|
author: "Autor X",
|
|
siteName: "Example",
|
|
imageUrl: nil,
|
|
wordCount: nil,
|
|
readingTimeMinutes: nil,
|
|
publishedAt: nil,
|
|
status: .unread,
|
|
readingProgress: 0,
|
|
isFavorite: false,
|
|
savedAt: "2026-05-18T11:00:00Z",
|
|
readAt: nil,
|
|
userNote: nil,
|
|
extractedVersion: 1,
|
|
createdAt: "2026-05-18T11:00:00Z",
|
|
updatedAt: "2026-05-18T11:00:00Z"
|
|
)
|
|
let item = SpotlightIndex.makeSearchableItem(article)
|
|
XCTAssertEqual(item.uniqueIdentifier, "abc-123")
|
|
XCTAssertEqual(item.domainIdentifier, SpotlightIndex.domainIdentifier)
|
|
XCTAssertEqual(item.attributeSet.title, "Beispiel-Artikel")
|
|
XCTAssertEqual(item.attributeSet.contentDescription, "Kurzer Auszug")
|
|
XCTAssertEqual(item.attributeSet.contentURL?.absoluteString, "pageta://article/abc-123")
|
|
XCTAssertTrue(item.attributeSet.keywords?.contains("Example") == true)
|
|
XCTAssertTrue(item.attributeSet.keywords?.contains("Autor X") == true)
|
|
XCTAssertTrue(item.attributeSet.keywords?.contains("Ungelesen") == true)
|
|
}
|
|
#endif
|
|
|
|
// MARK: - SidebarSection (RootView)
|
|
|
|
func testSidebarSection_allCases_haveLabelAndIcon() {
|
|
for section in SidebarSection.allCases {
|
|
XCTAssertFalse(section.label.isEmpty)
|
|
XCTAssertFalse(section.icon.isEmpty)
|
|
}
|
|
}
|
|
|
|
func testSidebarSection_rawValue_roundTrip() {
|
|
// SceneStorage persistiert über rawValue — Drift bei Enum-
|
|
// Umbenennen würde stale Selections zurückwerfen.
|
|
for section in SidebarSection.allCases {
|
|
XCTAssertEqual(SidebarSection(rawValue: section.rawValue), section)
|
|
}
|
|
}
|
|
|
|
// MARK: - WidgetSnapshot
|
|
|
|
func testWidgetSnapshot_decodesFromWireFormat() throws {
|
|
let json = """
|
|
{
|
|
"feed": [
|
|
{
|
|
"id": "feed-1",
|
|
"title": "Sample",
|
|
"siteName": "Example",
|
|
"topic": "tech",
|
|
"imageUrl": null,
|
|
"publishedAt": null,
|
|
"url": "https://example.com/x"
|
|
}
|
|
],
|
|
"saved": { "unread": 4, "reading": 1, "finished": 12, "archived": 38 },
|
|
"lastSyncAt": "2026-05-18T16:00:00Z"
|
|
}
|
|
""".data(using: .utf8)!
|
|
let snap = try JSONDecoder().decode(WidgetSnapshot.self, from: json)
|
|
XCTAssertEqual(snap.feed.count, 1)
|
|
XCTAssertEqual(snap.feed.first?.id, "feed-1")
|
|
XCTAssertEqual(snap.saved.unread, 4)
|
|
XCTAssertEqual(snap.saved.archived, 38)
|
|
}
|
|
|
|
func testWidgetSnapshotStore_roundTrip() throws {
|
|
let key = "pageta.tests.widget.\(UUID().uuidString)"
|
|
let store = WidgetSnapshotStore(appGroup: "test.\(UUID().uuidString)", key: key)
|
|
store.clear()
|
|
XCTAssertNil(store.load())
|
|
|
|
let snap = WidgetSnapshot(
|
|
feed: [WidgetFeedItem(id: "a", title: "T", siteName: "S", topic: "tech", imageUrl: nil, publishedAt: nil, url: "https://x")],
|
|
saved: WidgetSnapshot.SavedCounts(unread: 1, reading: 0, finished: 2, archived: 3),
|
|
lastSyncAt: "2026-05-18T16:00:00Z"
|
|
)
|
|
try store.save(snap)
|
|
XCTAssertEqual(store.load(), snap)
|
|
store.clear()
|
|
XCTAssertNil(store.load())
|
|
}
|
|
|
|
// MARK: - Highlight
|
|
|
|
func testHighlightColor_allCases_haveLabelsAndColor() {
|
|
for color in HighlightColor.allCases {
|
|
XCTAssertFalse(color.label.isEmpty, "Color \(color.rawValue) has empty label")
|
|
}
|
|
}
|
|
|
|
func testHighlightColor_matchesWireFormat() {
|
|
let expected: Set<String> = ["yellow", "green", "blue", "pink"]
|
|
let actual = Set(HighlightColor.allCases.map(\.rawValue))
|
|
XCTAssertEqual(actual, expected)
|
|
}
|
|
|
|
func testHighlight_decodesFromWireFormat() throws {
|
|
let json = """
|
|
{
|
|
"id": "hl-1",
|
|
"articleId": "art-1",
|
|
"userId": "user-42",
|
|
"text": "Eine markierte Passage.",
|
|
"note": null,
|
|
"color": "yellow",
|
|
"startOffset": 0,
|
|
"endOffset": 23,
|
|
"contextBefore": null,
|
|
"contextAfter": null,
|
|
"createdAt": "2026-05-18T11:00:00Z",
|
|
"updatedAt": "2026-05-18T11:00:00Z"
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let highlight = try JSONDecoder().decode(Highlight.self, from: json)
|
|
XCTAssertEqual(highlight.id, "hl-1")
|
|
XCTAssertEqual(highlight.color, .yellow)
|
|
XCTAssertEqual(highlight.startOffset, 0)
|
|
XCTAssertEqual(highlight.endOffset, 23)
|
|
XCTAssertNil(highlight.note)
|
|
}
|
|
}
|