Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add streams API endpoint for fetching Reader cards #744

Merged
merged 3 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ _None._

### New Features

_None._
- Add Reader discover streams endpoint. [#744]

### Bug Fixes

Expand Down
83 changes: 66 additions & 17 deletions WordPressKit/ReaderPostServiceRemote+Cards.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public enum ReaderSortingOption: String, CaseIterable {
}

extension ReaderPostServiceRemote {
/// Returns a collection of RemoteReaderCard
/// Returns a collection of RemoteReaderCard using the tags API
/// a Reader Card can represent an item for the reader feed, such as
/// - Reader Post
/// - Topics you may like
Expand All @@ -29,45 +29,94 @@ extension ReaderPostServiceRemote {
refreshCount: Int? = nil,
success: @escaping ([RemoteReaderCard], String?) -> Void,
failure: @escaping (Error) -> Void) {
guard let requestUrl = cardsEndpoint(for: topics, page: page, sortingOption: sortingOption, refreshCount: refreshCount) else {
let path = "read/tags/cards"
guard let requestUrl = cardsEndpoint(with: path,
topics: topics,
page: page,
sortingOption: sortingOption,
refreshCount: refreshCount) else {
return
}
fetch(requestUrl, success: success, failure: failure)
}

/// Returns a collection of RemoteReaderCard using the discover streams API
/// a Reader Card can represent an item for the reader feed, such as
/// - Reader Post
/// - Topics you may like
/// - Blogs you may like and so on
///
/// - Parameter topics: an array of String representing the topics
/// - Parameter page: a String that represents a page handle
/// - Parameter sortingOption: a ReaderSortingOption that represents a sorting option
/// - Parameter count: the number of cards to fetch. Warning: This also changes the number of objects returned for recommended sites/tags.
/// - Parameter success: Called when the request succeeds and the data returned is valid
/// - Parameter failure: Called if the request fails for any reason, or the response data is invalid
public func fetchStreamCards(for topics: [String],
page: String? = nil,
sortingOption: ReaderSortingOption = .noSorting,
refreshCount: Int? = nil,
count: Int? = nil,
success: @escaping ([RemoteReaderCard], String?) -> Void,
failure: @escaping (Error) -> Void) {
let path = "read/streams/discover"
guard let requestUrl = cardsEndpoint(with: path,
topics: topics,
page: page,
sortingOption: sortingOption,
count: count,
refreshCount: refreshCount) else {
return
}
fetch(requestUrl, success: success, failure: failure)
}

wordPressComRestApi.GET(requestUrl,
private func fetch(_ endpoint: String,
success: @escaping ([RemoteReaderCard], String?) -> Void,
failure: @escaping (Error) -> Void) {
wordPressComRestApi.GET(endpoint,
parameters: nil,
success: { response, _ in
do {
let decoder = JSONDecoder()
let data = try JSONSerialization.data(withJSONObject: response, options: [])
let envelope = try decoder.decode(ReaderCardEnvelope.self, from: data)
wargcm marked this conversation as resolved.
Show resolved Hide resolved

do {
let decoder = JSONDecoder()
let data = try JSONSerialization.data(withJSONObject: response, options: [])
let envelope = try decoder.decode(ReaderCardEnvelope.self, from: data)

success(envelope.cards, envelope.nextPageHandle)
} catch {
WPKitLogError("Error parsing the reader cards response: \(error)")
failure(error)
}
success(envelope.cards, envelope.nextPageHandle)
} catch {
WPKitLogError("Error parsing the reader cards response: \(error)")
failure(error)
}
}, failure: { error, _ in
WPKitLogError("Error fetching reader cards: \(error)")

failure(error)
})
}

private func cardsEndpoint(for topics: [String], page: String? = nil, sortingOption: ReaderSortingOption = .noSorting, refreshCount: Int? = nil) -> String? {
var path = URLComponents(string: "read/tags/cards")
private func cardsEndpoint(with path: String,
topics: [String],
page: String? = nil,
sortingOption: ReaderSortingOption = .noSorting,
count: Int? = nil,
refreshCount: Int? = nil) -> String? {
var path = URLComponents(string: path)

path?.queryItems = topics.map { URLQueryItem(name: "tags[]", value: $0) }

if let page = page {
if let page {
path?.queryItems?.append(URLQueryItem(name: "page_handle", value: page))
}

if let sortingOption = sortingOption.queryValue {
path?.queryItems?.append(URLQueryItem(name: "sort", value: sortingOption))
}

if let refreshCount = refreshCount {
if let count {
path?.queryItems?.append(URLQueryItem(name: "count", value: String(count)))
}

if let refreshCount {
path?.queryItems?.append(URLQueryItem(name: "refresh", value: String(refreshCount)))
}

Expand Down
138 changes: 138 additions & 0 deletions WordPressKitTests/ReaderPostServiceRemote+CardsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class ReaderPostServiceRemoteCardTests: RemoteTestCase, RESTTestable {
readerPostServiceRemote = ReaderPostServiceRemote(wordPressComRestApi: getRestApi())
}

// MARK: - Tags fetch cards

// Return an array of cards
//
func testReturnCards() {
Expand Down Expand Up @@ -153,4 +155,140 @@ class ReaderPostServiceRemoteCardTests: RemoteTestCase, RESTTestable {

waitForExpectations(timeout: timeout, handler: nil)
}

// MARK: - Streams fetch cards

func testStreamsReturnCards() {
let expect = expectation(description: "Get cards successfully")
stubRemoteResponse("read/streams/discover?tags%5B%5D=dogs", filename: "reader-cards-success.json", contentType: .ApplicationJSON)

readerPostServiceRemote.fetchStreamCards(for: ["dogs"], success: { cards, _ in
XCTAssertTrue(cards.count == 10)
expect.fulfill()
}, failure: { _ in })

waitForExpectations(timeout: timeout, handler: nil)
}

func testStreamsReturnPosts() {
let expect = expectation(description: "Get cards successfully")
stubRemoteResponse("read/streams/discover?tags%5B%5D=cats", filename: "reader-cards-success.json", contentType: .ApplicationJSON)

readerPostServiceRemote.fetchStreamCards(for: ["cats"], success: { cards, _ in
let postCards = cards.filter { $0.type == .post }
XCTAssertTrue(postCards.allSatisfy { $0.post != nil })
expect.fulfill()
}, failure: { _ in })

waitForExpectations(timeout: timeout, handler: nil)
}

func testStreamsReturnCorrectCardType() {
let expect = expectation(description: "Get cards successfully")
stubRemoteResponse("read/streams/discover?tags%5B%5D=cats", filename: "reader-cards-success.json", contentType: .ApplicationJSON)

readerPostServiceRemote.fetchStreamCards(for: ["cats"], success: { cards, _ in
let postTypes = cards.map { $0.type }
let expectedPostTypes: [RemoteReaderCard.CardType] = [.interests, .sites, .post, .post, .post, .post, .post, .post, .post, .post]
XCTAssertTrue(postTypes == expectedPostTypes)
expect.fulfill()
}, failure: { _ in })

waitForExpectations(timeout: timeout, handler: nil)
}

func testStreamsReturnError() {
let expect = expectation(description: "Get cards successfully")
stubRemoteResponse("read/streams/discover?tags%5B%5D=cats", filename: "reader-cards-success.json", contentType: .ApplicationJSON, status: 503)

readerPostServiceRemote.fetchStreamCards(for: ["cats"], success: { _, _ in }, failure: { error in
XCTAssertNotNil(error)
expect.fulfill()
})

waitForExpectations(timeout: timeout, handler: nil)
}

func testStreamsReturnNextPageHandle() {
let expect = expectation(description: "Returns next page handle")
stubRemoteResponse("read/streams/discover?tags%5B%5D=dogs", filename: "reader-cards-success.json", contentType: .ApplicationJSON)

readerPostServiceRemote.fetchStreamCards(for: ["dogs"], success: { _, nextPageHandle in
XCTAssertTrue(nextPageHandle == "ZnJvbT0xMCZiZWZvcmU9MjAyMC0wNy0yNlQxMyUzQTU1JTNBMDMlMkIwMSUzQTAw")
expect.fulfill()
}, failure: { _ in })

waitForExpectations(timeout: timeout, handler: nil)
}

func testStreamsCallAPIWithTheGivenPageHandle() {
let readerPostServiceRemote = ReaderPostServiceRemote(wordPressComRestApi: mockRemoteApi)

readerPostServiceRemote.fetchStreamCards(for: ["dogs"], page: "foobar", success: { _, _ in }, failure: { _ in })

XCTAssertTrue(mockRemoteApi.getMethodCalled)

XCTAssertTrue(mockRemoteApi.URLStringPassedIn?.contains("&page_handle=foobar") ?? false)
}

func testStreamsCallAPIWithPopularityAsTheGivenSortingOption() {
let readerPostServiceRemote = ReaderPostServiceRemote(wordPressComRestApi: mockRemoteApi)

readerPostServiceRemote.fetchStreamCards(for: [], sortingOption: .popularity, success: { _, _ in }, failure: { _ in })

XCTAssertTrue(mockRemoteApi.getMethodCalled)

XCTAssertTrue(mockRemoteApi.URLStringPassedIn?.contains("sort=popularity") ?? false)
}

func testStreamsCallAPIWithDateAsTheGivenSortingOption() {
let readerPostServiceRemote = ReaderPostServiceRemote(wordPressComRestApi: mockRemoteApi)

readerPostServiceRemote.fetchStreamCards(for: [], sortingOption: .date, success: { _, _ in }, failure: { _ in })

XCTAssertTrue(mockRemoteApi.getMethodCalled)

XCTAssertTrue(mockRemoteApi.URLStringPassedIn?.contains("sort=date") ?? false)
}

func testStreamsCallAPIWithoutTheGivenSortingOption() {
let readerPostServiceRemote = ReaderPostServiceRemote(wordPressComRestApi: mockRemoteApi)

readerPostServiceRemote.fetchStreamCards(for: [], success: { _, _ in }, failure: { _ in })

XCTAssertTrue(mockRemoteApi.getMethodCalled)

XCTAssertFalse(mockRemoteApi.URLStringPassedIn?.contains("sort=") ?? true)
}

func testStreamsCallAPIWithCountValue() {
let readerPostServiceRemote = ReaderPostServiceRemote(wordPressComRestApi: mockRemoteApi)
let expectedCount = 5

readerPostServiceRemote.fetchStreamCards(for: ["dogs"], count: expectedCount, success: { _, _ in }, failure: { _ in })

XCTAssertTrue(mockRemoteApi.getMethodCalled)

XCTAssertTrue(mockRemoteApi.URLStringPassedIn?.contains("&count=\(expectedCount)") ?? false)
}

func testStreamsPostsInCallAPIWithDateAsGivenSortOption() {
let expect = expectation(description: "Get cards sorted by date")
stubRemoteResponse("read/streams/discover?tags%5B%5D=cats&sort=date", filename: "reader-cards-success.json", contentType: .ApplicationJSON)

readerPostServiceRemote.fetchStreamCards(for: ["cats"], sortingOption: .date, success: { cards, _ in
let posts = cards.filter { $0.type == .post }
for i in 1..<posts.count {
guard let firstPostDate = posts[i-1].post?.sortDate,
let secondPostDate = posts[i].post?.sortDate,
firstPostDate > secondPostDate else {
XCTFail("Posts should be sorted by date, starting with most recent post")
return
}
}
expect.fulfill()
}, failure: { _ in })

waitForExpectations(timeout: timeout, handler: nil)
}
}