Merge branch 'ios-candidate' of https://github.com/Ranchero-Software/NetNewsWire into ios-candidate

This commit is contained in:
Maurice Parker 2020-01-30 18:49:09 -07:00
commit 3a69425752
11 changed files with 444 additions and 29 deletions

View File

@ -111,6 +111,9 @@
9E44C90F23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E44C90E23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift */; };
9E5ABE9A236BE6BD00B5DE9F /* feedly-1-initial in Resources */ = {isa = PBXBuildFile; fileRef = 9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */; };
9E5DE60E23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5DE60D23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift */; };
9E5EC15923E01D8A00A4E503 /* FeedlyCollectionParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5EC15823E01D8A00A4E503 /* FeedlyCollectionParser.swift */; };
9E5EC15B23E01DEF00A4E503 /* FeedlyRTLTextSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5EC15A23E01DEF00A4E503 /* FeedlyRTLTextSanitizer.swift */; };
9E5EC15D23E0D58500A4E503 /* FeedlyFeedParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5EC15C23E0D58500A4E503 /* FeedlyFeedParser.swift */; };
9E672394236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E672393236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift */; };
9E672396236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E672395236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift */; };
9E7299D723505E9600DAEFB7 /* FeedlyAddFeedToCollectionOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7299D623505E9600DAEFB7 /* FeedlyAddFeedToCollectionOperation.swift */; };
@ -164,6 +167,10 @@
9EF1B10723590D61000A486A /* FeedlyGetStreamIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF1B10623590D61000A486A /* FeedlyGetStreamIdsOperation.swift */; };
9EF1B10923590E93000A486A /* FeedlyStreamIds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF1B10823590E93000A486A /* FeedlyStreamIds.swift */; };
9EF2602C23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF2602B23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift */; };
9EF58EB023E1606000992A2B /* FeedlyTextSanitizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF58EAF23E1606000992A2B /* FeedlyTextSanitizationTests.swift */; };
9EF58EB223E1647400992A2B /* FeedlyCollectionParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF58EB123E1647400992A2B /* FeedlyCollectionParserTests.swift */; };
9EF58EB423E1655300992A2B /* FeedlyFeedParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF58EB323E1655300992A2B /* FeedlyFeedParserTests.swift */; };
9EF58EB623E1669F00992A2B /* FeedlyEntryParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF58EB523E1669F00992A2B /* FeedlyEntryParserTests.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -327,6 +334,9 @@
9E489E92236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyUpdateAccountFeedsWithItemsOperationTests.swift; sourceTree = "<group>"; };
9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-1-initial"; sourceTree = "<group>"; };
9E5DE60D23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFetchIdsForMissingArticlesOperation.swift; sourceTree = "<group>"; };
9E5EC15823E01D8A00A4E503 /* FeedlyCollectionParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCollectionParser.swift; sourceTree = "<group>"; };
9E5EC15A23E01DEF00A4E503 /* FeedlyRTLTextSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRTLTextSanitizer.swift; sourceTree = "<group>"; };
9E5EC15C23E0D58500A4E503 /* FeedlyFeedParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeedParser.swift; sourceTree = "<group>"; };
9E672393236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRefreshAccessTokenOperation.swift; sourceTree = "<group>"; };
9E672395236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAcessTokenRefreshing.swift; sourceTree = "<group>"; };
9E7299D623505E9600DAEFB7 /* FeedlyAddFeedToCollectionOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddFeedToCollectionOperation.swift; sourceTree = "<group>"; };
@ -386,6 +396,10 @@
9EF1B10623590D61000A486A /* FeedlyGetStreamIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamIdsOperation.swift; sourceTree = "<group>"; };
9EF1B10823590E93000A486A /* FeedlyStreamIds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyStreamIds.swift; sourceTree = "<group>"; };
9EF2602B23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetUpdatedArticleIdsOperation.swift; sourceTree = "<group>"; };
9EF58EAF23E1606000992A2B /* FeedlyTextSanitizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyTextSanitizationTests.swift; sourceTree = "<group>"; };
9EF58EB123E1647400992A2B /* FeedlyCollectionParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCollectionParserTests.swift; sourceTree = "<group>"; };
9EF58EB323E1655300992A2B /* FeedlyFeedParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeedParserTests.swift; sourceTree = "<group>"; };
9EF58EB523E1669F00992A2B /* FeedlyEntryParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyEntryParserTests.swift; sourceTree = "<group>"; };
D511EEB5202422BB00712EC3 /* Account_project_debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_debug.xcconfig; sourceTree = "<group>"; };
D511EEB6202422BB00712EC3 /* Account_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_target.xcconfig; sourceTree = "<group>"; };
D511EEB7202422BB00712EC3 /* Account_project_release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_release.xcconfig; sourceTree = "<group>"; };
@ -652,6 +666,10 @@
9E0260CA236FF99A00D122D3 /* FeedlyRefreshAccessTokenOperationTests.swift */,
9E784EBF237E8BE100099B1B /* FeedlyLogoutOperationTests.swift */,
9EA643D823945CE00018A28C /* FeedlyAddNewFeedOperationTests.swift */,
9EF58EAF23E1606000992A2B /* FeedlyTextSanitizationTests.swift */,
9EF58EB123E1647400992A2B /* FeedlyCollectionParserTests.swift */,
9EF58EB323E1655300992A2B /* FeedlyFeedParserTests.swift */,
9EF58EB523E1669F00992A2B /* FeedlyEntryParserTests.swift */,
9E79F7732395C9EF0031DB98 /* feedly-add-new-feed */,
9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */,
9EC804E4236C1A7F0057CFCB /* feedly-2-changestatuses */,
@ -720,7 +738,9 @@
children = (
9E1773D823458D590056A5A8 /* FeedlyResourceId.swift */,
9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */,
9E5EC15823E01D8A00A4E503 /* FeedlyCollectionParser.swift */,
9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */,
9E5EC15C23E0D58500A4E503 /* FeedlyFeedParser.swift */,
9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */,
9E1773D4234570E30056A5A8 /* FeedlyEntryParser.swift */,
9EAEC625233318400085D7C9 /* FeedlyStream.swift */,
@ -731,6 +751,7 @@
9E1773D6234575AB0056A5A8 /* FeedlyTag.swift */,
9EA643D4239306AC0018A28C /* FeedlyFeedsSearchResponse.swift */,
9EBD49C123C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift */,
9E5EC15A23E01DEF00A4E503 /* FeedlyRTLTextSanitizer.swift */,
);
path = Models;
sourceTree = "<group>";
@ -977,10 +998,12 @@
51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */,
846E77451F6EF9B900A165E2 /* Container.swift in Sources */,
9EA643D3239305680018A28C /* FeedlySearchOperation.swift in Sources */,
9E5EC15D23E0D58500A4E503 /* FeedlyFeedParser.swift in Sources */,
9E1D15532334304B00F4944C /* FeedlyGetStreamContentsOperation.swift in Sources */,
9E12B0202334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift in Sources */,
552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */,
9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */,
9E5EC15B23E01DEF00A4E503 /* FeedlyRTLTextSanitizer.swift in Sources */,
511B9804237CD4270028BCAA /* FeedIdentifier.swift in Sources */,
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */,
841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */,
@ -995,6 +1018,7 @@
9EEAE06E235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift in Sources */,
5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */,
9E85C8ED2367020700D0F1F7 /* FeedlyGetEntriesService.swift in Sources */,
9E5EC15923E01D8A00A4E503 /* FeedlyCollectionParser.swift in Sources */,
9E84DC492359A73600D6E809 /* FeedlyCheckpointOperation.swift in Sources */,
9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */,
9E1D154D233370D800F4944C /* FeedlySyncAllOperation.swift in Sources */,
@ -1081,6 +1105,7 @@
9EC228572362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift in Sources */,
9E03C122235E62E100FB6D9E /* FeedlyTestSupport.swift in Sources */,
9EAADA1023C93144003A801F /* TestGetEntriesService.swift in Sources */,
9EF58EB423E1655300992A2B /* FeedlyFeedParserTests.swift in Sources */,
9E1FF8622368219B00834C24 /* TestGetPagedStreamIdsService.swift in Sources */,
9E03C11E235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift in Sources */,
9E85C8E62366FED600D0F1F7 /* TestGetStreamContentsService.swift in Sources */,
@ -1088,14 +1113,17 @@
9E03C11C235D921400FB6D9E /* FeedlyOperationTests.swift in Sources */,
9E1FF8602368216B00834C24 /* TestGetStreamIdsService.swift in Sources */,
9E85C8E82366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift in Sources */,
9EF58EB023E1606000992A2B /* FeedlyTextSanitizationTests.swift in Sources */,
5165D7122282080C00D9D53D /* AccountFeedbinFolderContentsSyncTest.swift in Sources */,
51D5875E227F643C00900287 /* AccountFeedbinFolderSyncTest.swift in Sources */,
9EF58EB623E1669F00992A2B /* FeedlyEntryParserTests.swift in Sources */,
9EC804E3236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift in Sources */,
9E1FF8682368EE4900834C24 /* TestGetCollectionsService.swift in Sources */,
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */,
513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */,
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */,
9E1773DB234593CF0056A5A8 /* FeedlyResourceIdTests.swift in Sources */,
9EF58EB223E1647400992A2B /* FeedlyCollectionParserTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -0,0 +1,28 @@
//
// FeedlyCollectionParserTests.swift
// AccountTests
//
// Created by Kiel Gillard on 29/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import XCTest
@testable import Account
class FeedlyCollectionParserTests: XCTestCase {
func testParsing() {
let collection = FeedlyCollection(feeds: [], label: "Test Collection", id: "test/collection/1")
let parser = FeedlyCollectionParser(collection: collection)
XCTAssertEqual(parser.folderName, collection.label)
XCTAssertEqual(parser.externalID, collection.id)
}
func testSanitization() {
let name = "Test Collection"
let collection = FeedlyCollection(feeds: [], label: "<div style=\"direction:rtl;text-align:right\">\(name)</div>", id: "test/collection/1")
let parser = FeedlyCollectionParser(collection: collection)
XCTAssertEqual(parser.folderName, name)
XCTAssertEqual(parser.externalID, collection.id)
}
}

View File

@ -0,0 +1,196 @@
//
// FeedlyEntryParserTests.swift
// AccountTests
//
// Created by Kiel Gillard on 29/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import XCTest
@testable import Account
class FeedlyEntryParserTests: XCTestCase {
func testParsing() {
let content = FeedlyEntry.Content(content: "Test Content", direction: .leftToRight)
let summary = FeedlyEntry.Content(content: "Test Summary", direction: .leftToRight)
let origin = FeedlyOrigin(title: "Test Feed", streamId: "tests://feeds/1", htmlUrl: nil)
let canonicalLink = FeedlyLink(href: "tests://feeds/1/entries/1", type: "text/html")
let tags = [
FeedlyTag(id: "tests/tags/1", label: "Tag 1"),
FeedlyTag(id: "tests/tags/2", label: "Tag 2")
]
let entry = FeedlyEntry(id: "tests/feeds/1/entries/1",
title: "Test Entry 1",
content: content,
summary: summary,
author: "Bob Alice",
crawled: .distantPast,
recrawled: Date(timeIntervalSinceReferenceDate: 0),
origin: origin,
canonical: [canonicalLink],
alternate: nil,
unread: false,
tags: tags,
categories: nil,
enclosure: nil)
let parser = FeedlyEntryParser(entry: entry)
XCTAssertEqual(parser.id, entry.id)
XCTAssertEqual(parser.feedUrl, origin.streamId)
XCTAssertEqual(parser.externalUrl, canonicalLink.href)
XCTAssertEqual(parser.title, entry.title)
XCTAssertEqual(parser.contentHMTL, content.content)
XCTAssertEqual(parser.summary, summary.content)
XCTAssertEqual(parser.datePublished, .distantPast)
XCTAssertEqual(parser.dateModified, Date(timeIntervalSinceReferenceDate: 0))
guard let item = parser.parsedItemRepresentation else {
XCTFail("Expected a parsed item representation.")
return
}
XCTAssertEqual(item.syncServiceID, entry.id)
XCTAssertEqual(item.uniqueID, entry.id)
// The following is not an error.
// The feedURL must match the webFeedID for the article to be connected to its matching feed.
XCTAssertEqual(item.feedURL, origin.streamId)
XCTAssertEqual(item.title, entry.title)
XCTAssertEqual(item.contentHTML, content.content)
XCTAssertEqual(item.contentText, nil, "Is it now free of HTML characters?")
XCTAssertEqual(item.summary, summary.content)
XCTAssertEqual(item.datePublished, entry.crawled)
XCTAssertEqual(item.dateModified, entry.recrawled)
let expectedTags = Set(tags.compactMap { $0.label })
XCTAssertEqual(item.tags, expectedTags)
let expectedAuthors = Set([entry.author])
let calculatedAuthors = Set(item.authors?.compactMap { $0.name } ?? [])
XCTAssertEqual(calculatedAuthors, expectedAuthors)
}
func testSanitization() {
let content = FeedlyEntry.Content(content: "<div style=\"direction:rtl;text-align:right\">Test Content</div>", direction: .rightToLeft)
let summaryContent = "Test Summary"
let summary = FeedlyEntry.Content(content: "<div style=\"direction:rtl;text-align:right\">\(summaryContent)</div>", direction: .rightToLeft)
let origin = FeedlyOrigin(title: "Test Feed", streamId: "tests://feeds/1", htmlUrl: nil)
let title = "Test Entry 1"
let entry = FeedlyEntry(id: "tests/feeds/1/entries/1",
title: "<div style=\"direction:rtl;text-align:right\">\(title)</div>",
content: content,
summary: summary,
author: nil,
crawled: .distantPast,
recrawled: nil,
origin: origin,
canonical: nil,
alternate: nil,
unread: false,
tags: nil,
categories: nil,
enclosure: nil)
let parser = FeedlyEntryParser(entry: entry)
// These should be sanitized
XCTAssertEqual(parser.title, title)
XCTAssertEqual(parser.summary, summaryContent)
// These should not be sanitized because it is supposed to be HTML content.
XCTAssertEqual(parser.contentHMTL, content.content)
}
func testLocatesCanonicalExternalUrl() {
let canonicalLink = FeedlyLink(href: "tests://feeds/1/entries/1", type: "text/html")
let alternateLink = FeedlyLink(href: "tests://feeds/1/entries/alternate/1", type: "text/html")
let entry = FeedlyEntry(id: "tests/feeds/1/entries/1",
title: "Test Entry 1",
content: nil,
summary: nil,
author: nil,
crawled: .distantPast,
recrawled: Date(timeIntervalSinceReferenceDate: 0),
origin: nil,
canonical: [canonicalLink],
alternate: [alternateLink],
unread: false,
tags: nil,
categories: nil,
enclosure: nil)
let parser = FeedlyEntryParser(entry: entry)
XCTAssertEqual(parser.externalUrl, canonicalLink.href)
}
func testLocatesAlternateExternalUrl() {
let canonicalLink = FeedlyLink(href: "tests://feeds/1/entries/1", type: "text/json")
let alternateLink = FeedlyLink(href: "tests://feeds/1/entries/alternate/1", type: nil)
let entry = FeedlyEntry(id: "tests/feeds/1/entries/1",
title: "Test Entry 1",
content: nil,
summary: nil,
author: nil,
crawled: .distantPast,
recrawled: Date(timeIntervalSinceReferenceDate: 0),
origin: nil,
canonical: [canonicalLink],
alternate: [alternateLink],
unread: false,
tags: nil,
categories: nil,
enclosure: nil)
let parser = FeedlyEntryParser(entry: entry)
XCTAssertEqual(parser.externalUrl, alternateLink.href)
}
func testContentPreferredToSummary() {
let content = FeedlyEntry.Content(content: "Test Content", direction: .leftToRight)
let summary = FeedlyEntry.Content(content: "Test Summary", direction: .leftToRight)
let entry = FeedlyEntry(id: "tests/feeds/1/entries/1",
title: "Test Entry 1",
content: content,
summary: summary,
author: nil,
crawled: .distantPast,
recrawled: Date(timeIntervalSinceReferenceDate: 0),
origin: nil,
canonical: nil,
alternate: nil,
unread: false,
tags: nil,
categories: nil,
enclosure: nil)
let parser = FeedlyEntryParser(entry: entry)
XCTAssertEqual(parser.contentHMTL, content.content)
}
func testSummaryUsedAsContentWhenContentMissing() {
let summary = FeedlyEntry.Content(content: "Test Summary", direction: .leftToRight)
let entry = FeedlyEntry(id: "tests/feeds/1/entries/1",
title: "Test Entry 1",
content: nil,
summary: summary,
author: nil,
crawled: .distantPast,
recrawled: Date(timeIntervalSinceReferenceDate: 0),
origin: nil,
canonical: nil,
alternate: nil,
unread: false,
tags: nil,
categories: nil,
enclosure: nil)
let parser = FeedlyEntryParser(entry: entry)
XCTAssertEqual(parser.contentHMTL, summary.content)
}
}

View File

@ -0,0 +1,41 @@
//
// FeedlyFeedParserTests.swift
// AccountTests
//
// Created by Kiel Gillard on 29/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import XCTest
@testable import Account
class FeedlyFeedParserTests: XCTestCase {
func testParsing() {
let name = "Test Feed"
let website = "tests://nnw/feed/1"
let url = "tests://nnw/feed.xml"
let id = "feed/\(url)"
let updated = Date.distantPast
let feed = FeedlyFeed(id: id, title: name, updated: updated, website: website)
let parser = FeedlyFeedParser(feed: feed)
XCTAssertEqual(parser.title, name)
XCTAssertEqual(parser.homePageURL, website)
XCTAssertEqual(parser.url, url)
XCTAssertEqual(parser.webFeedID, id)
}
func testSanitization() {
let name = "Test Feed"
let website = "tests://nnw/feed/1"
let url = "tests://nnw/feed.xml"
let id = "feed/\(url)"
let updated = Date.distantPast
let feed = FeedlyFeed(id: id, title: "<div style=\"direction:rtl;text-align:right\">\(name)</div>", updated: updated, website: website)
let parser = FeedlyFeedParser(feed: feed)
XCTAssertEqual(parser.title, name)
XCTAssertEqual(parser.homePageURL, website)
XCTAssertEqual(parser.url, url)
XCTAssertEqual(parser.webFeedID, id)
}
}

View File

@ -0,0 +1,38 @@
//
// FeedlyTextSanitizationTests.swift
// AccountTests
//
// Created by Kiel Gillard on 29/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import XCTest
@testable import Account
class FeedlyTextSanitizationTests: XCTestCase {
func testRTLSanitization() {
let targetsAndExpectations: [(target: String?, expectation: String?)] = [
(nil, nil),
("", ""),
(" ", " "),
("text", "text"),
("<div style=\"direction:rtl;text-align:right\">", "<div style=\"direction:rtl;text-align:right\">"),
("</div>", "</div>"),
("<div style=\"direction:rtl;text-align:right\">text", "<div style=\"direction:rtl;text-align:right\">text"),
("text</div>", "text</div>"),
("<div style=\"direction:rtl;text-align:right\"></div>", ""),
("<DIV style=\"direction:rtl;text-align:right\"></div>", "<DIV style=\"direction:rtl;text-align:right\"></div>"),
("<div style=\"direction:rtl;text-align:right\"></DIV>", "<div style=\"direction:rtl;text-align:right\"></DIV>"),
("<div style=\"direction:rtl;text-align:right\">text</div>", "text"),
]
let sanitizer = FeedlyRTLTextSanitizer()
for (target, expectation) in targetsAndExpectations {
let calculated = sanitizer.sanitize(target)
XCTAssertEqual(expectation, calculated)
}
}
}

View File

@ -0,0 +1,23 @@
//
// FeedlyCollectionParser.swift
// Account
//
// Created by Kiel Gillard on 28/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct FeedlyCollectionParser {
let collection: FeedlyCollection
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
var folderName: String {
return rightToLeftTextSantizer.sanitize(collection.label) ?? ""
}
var externalID: String {
return collection.id
}
}

View File

@ -13,13 +13,17 @@ import RSParser
struct FeedlyEntryParser {
let entry: FeedlyEntry
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
var id: String {
return entry.id
}
/// When ingesting articles, the feedURL must match a feed's `webFeedID` for the article to be reachable between it and its matching feed. It reminds me of a foreign key.
var feedUrl: String? {
guard let id = entry.origin?.streamId else {
assertionFailure()
// At this point, check Feedly's API isn't glitching or the response has not changed structure.
assertionFailure("Entries need to be traceable to a feed or this entry will be dropped.")
return nil
}
return id
@ -36,7 +40,7 @@ struct FeedlyEntryParser {
}
var title: String? {
return entry.title
return rightToLeftTextSantizer.sanitize(entry.title)
}
var contentHMTL: String? {
@ -49,7 +53,7 @@ struct FeedlyEntryParser {
}
var summary: String? {
return entry.summary?.content
return rightToLeftTextSantizer.sanitize(entry.summary?.content)
}
var datePublished: Date {
@ -67,6 +71,7 @@ struct FeedlyEntryParser {
return Set([ParsedAuthor(name: name, url: nil, avatarURL: nil, emailAddress: nil)])
}
/// While there is not yet a tagging interface, articles can still be searched for by tags.
var tags: Set<String>? {
guard let labels = entry.tags?.compactMap({ $0.label }), !labels.isEmpty else {
return nil

View File

@ -0,0 +1,32 @@
//
// FeedlyFeedParser.swift
// Account
//
// Created by Kiel Gillard on 29/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct FeedlyFeedParser {
let feed: FeedlyFeed
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
var title: String? {
return rightToLeftTextSantizer.sanitize(feed.title) ?? ""
}
var webFeedID: String {
return feed.id
}
var url: String {
let resource = FeedlyFeedResourceId(id: feed.id)
return resource.url
}
var homePageURL: String? {
return feed.website
}
}

View File

@ -0,0 +1,28 @@
//
// FeedlyRTLTextSanitizer.swift
// Account
//
// Created by Kiel Gillard on 28/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct FeedlyRTLTextSanitizer {
private let rightToLeftPrefix = "<div style=\"direction:rtl;text-align:right\">"
private let rightToLeftSuffix = "</div>"
func sanitize(_ sourceText: String?) -> String? {
guard let source = sourceText, !source.isEmpty else {
return sourceText
}
guard source.hasPrefix(rightToLeftPrefix) && source.hasSuffix(rightToLeftSuffix) else {
return source
}
let start = source.index(source.startIndex, offsetBy: rightToLeftPrefix.indices.count)
let end = source.index(source.endIndex, offsetBy: -rightToLeftSuffix.indices.count)
return String(source[start..<end])
}
}

View File

@ -68,9 +68,11 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
}
// no exsiting feed, create a new one
let id = collectionFeed.id
let url = FeedlyFeedResourceId(id: id).url
let feed = account.createWebFeed(with: collectionFeed.title, url: url, webFeedID: id, homePageURL: collectionFeed.website)
let parser = FeedlyFeedParser(feed: collectionFeed)
let feed = account.createWebFeed(with: parser.title,
url: parser.url,
webFeedID: parser.webFeedID,
homePageURL: parser.homePageURL)
// So the same feed isn't created more than once.
feedsAdded.insert(feed)

View File

@ -9,22 +9,17 @@
import Foundation
import os.log
protocol FeedlyCollectionsAndFoldersProviding: class {
var collectionsAndFolders: [(FeedlyCollection, Folder)] { get }
}
protocol FeedlyFeedsAndFoldersProviding {
var feedsAndFolders: [([FeedlyFeed], Folder)] { get }
}
/// Reflect Collections from Feedly as Folders.
final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlyOperation, FeedlyCollectionsAndFoldersProviding, FeedlyFeedsAndFoldersProviding {
final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlyOperation, FeedlyFeedsAndFoldersProviding {
let account: Account
let collectionsProvider: FeedlyCollectionProviding
let log: OSLog
private(set) var collectionsAndFolders = [(FeedlyCollection, Folder)]()
private(set) var feedsAndFolders = [([FeedlyFeed], Folder)]()
init(account: Account, collectionsProvider: FeedlyCollectionProviding, log: OSLog) {
@ -41,29 +36,28 @@ final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlyOperation, FeedlyCo
let localFolders = account.folders ?? Set()
let collections = collectionsProvider.collections
let pairs = collections.compactMap { collection -> (FeedlyCollection, Folder)? in
guard let folder = account.ensureFolder(with: collection.label) else {
feedsAndFolders = collections.compactMap { collection -> ([FeedlyFeed], Folder)? in
let parser = FeedlyCollectionParser(collection: collection)
guard let folder = account.ensureFolder(with: parser.folderName) else {
assertionFailure("Why wasn't a folder created?")
return nil
}
folder.externalID = collection.id
return (collection, folder)
}
collectionsAndFolders = pairs
os_log(.debug, log: log, "Ensured %i folders for %i collections.", pairs.count, collections.count)
feedsAndFolders = pairs.map { (collection, folder) -> (([FeedlyFeed], Folder)) in
folder.externalID = parser.externalID
return (collection.feeds, folder)
}
// Remove folders without a corresponding collection
let collectionFolders = Set(pairs.map { $0.1 })
let foldersWithoutCollections = localFolders.subtracting(collectionFolders)
for unmatched in foldersWithoutCollections {
account.removeFolder(unmatched)
}
os_log(.debug, log: log, "Ensured %i folders for %i collections.", feedsAndFolders.count, collections.count)
os_log(.debug, log: log, "Removed %i folders: %@", foldersWithoutCollections.count, foldersWithoutCollections.map { $0.externalID ?? $0.nameForDisplay })
// Remove folders without a corresponding collection
let collectionFolders = Set(feedsAndFolders.map { $0.1 })
let foldersWithoutCollections = localFolders.subtracting(collectionFolders)
if !foldersWithoutCollections.isEmpty {
for unmatched in foldersWithoutCollections {
account.removeFolder(unmatched)
}
os_log(.debug, log: log, "Removed %i folders: %@", foldersWithoutCollections.count, foldersWithoutCollections.map { $0.externalID ?? $0.nameForDisplay })
}
}
}