diff --git a/Libraries/iTunes/Kit/Endpoints/GetReviewsAPIEndpoint.swift b/Libraries/iTunes/Kit/Endpoints/GetReviewsAPIEndpoint.swift new file mode 100644 index 0000000..1e8bdfb --- /dev/null +++ b/Libraries/iTunes/Kit/Endpoints/GetReviewsAPIEndpoint.swift @@ -0,0 +1,70 @@ +// +// GetReviewsAPIEndpoint.swift +// ReviewsiTunesKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation +import ReviewsFoundationKit +import ReviewsFeedKit + +struct GetReviewsAPIEndpoint: GetReviewsEndpoint { + + // MARK: Constants + let host: URL + let decoder: JSONDecoder + let session: URLSession + + // MARK: Functions + @discardableResult func callAsFunction( + _ input: ReviewsFeedKit.GetReviewsInput + ) async throws -> ReviewsFeedKit.GetReviewsOutput { + let path = try makePath(with: input) + let url = { + if #available(iOS 16.0, *) { + host.appending(path: path) + } else { + host.appendingPathComponent(path) + } + }() + + let (data, response) = try await session.data(from: url) + + guard let urlResponse = response as? HTTPURLResponse else { + throw EndpointError.responseNotFound + } + + guard urlResponse.statusCode == 200 else { + throw EndpointError.requestFailed(statusCode: urlResponse.statusCode) + } + + let feed = try decoder.decode(Feed.self, from: data) + + return .init(reviews: feed.entries) + } + + func makePath(with input: GetReviewsInput) throws -> String { + guard + !input.appID.isEmpty, + !input.countryCode.isEmpty + else{ + throw EndpointError.inputParametersEmpty + } + + return .init( + format: .Format.recentReviewsPath, + input.countryCode, + input.appID + ) + } + +} + +// MARK: - String+Formats +private extension String { + enum Format { + static let recentReviewsPath = "/%@/rss/customerreviews/id=%@/sortby=mostrecent/json" + } +} diff --git a/Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift b/Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift new file mode 100644 index 0000000..8090436 --- /dev/null +++ b/Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift @@ -0,0 +1,214 @@ +// +// GetReviewsAPIEndpointTests.swift +// ReviewsiTunesTest +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import ReviewsFeedKit +import ReviewsFoundationKit +import XCTest + +@testable import ReviewsiTunesKit + +final class GetReviewsAPIEndpointTests: XCTestCase { + + // MARK: Properties + private var input: GetReviewsAPIEndpoint.Input! + private var sut: GetReviewsAPIEndpoint! + + // MARK: Setup + override func setUp() async throws { + sut = .init( + host: .iTunes, + decoder: .default, + session: .init(configuration: .mock) + ) + } + + // MARK: Functions tests + func testCallAsFunction_whenResponseOK_withOneReview() async throws { + // GIVEN + let reviews: [Review] = .one + + input = .init( + appID: "1234567890", + countryCode: "abc" + ) + + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: reviews) + ) + + // WHEN + let output = try await sut(input) + + // THEN + XCTAssertFalse(output.reviews.isEmpty) + XCTAssertEqual(output.reviews.count, reviews.count) + XCTAssertEqual(output.reviews, reviews) + } + + func testCallAsFunction_whenResponseOK_withManyReviews() async throws { + // GIVEN + let reviews: [Review] = .many + + input = .init( + appID: "1234567890", + countryCode: "abc" + ) + + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: reviews) + ) + + // WHEN + let output = try await sut(input) + + // THEN + XCTAssertFalse(output.reviews.isEmpty) + XCTAssertEqual(output.reviews.count, reviews.count) + XCTAssertEqual(output.reviews, reviews) + } + + func testCallAsFunction_whenResponseOK_withEmptyReviews() async throws { + // GIVEN + let reviews: [Review] = .empty + + input = .init( + appID: "1234567890", + countryCode: "abc" + ) + + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: reviews) + ) + + // WHEN + let output = try await sut(input) + + // THEN + XCTAssertTrue(output.reviews.isEmpty) + XCTAssertEqual(output.reviews.count, reviews.count) + XCTAssertEqual(output.reviews, reviews) + } + + func testCallAsFunction_whenResponseNotOK() async throws { + // GIVEN + let statusCode = 404 + + input = .init( + appID: "1234567890", + countryCode: "abc" + ) + + MockURLProtocol.response = .init(statusCode: statusCode) + + // WHEN + // THEN + do { + try await sut(input) + } catch EndpointError.requestFailed(statusCode: statusCode) { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func testMakePath_withFilledInput() throws { + // GIVEN + input = .init( + appID: "1234567890", + countryCode: "abc" + ) + + // WHEN + let path = try sut.makePath(with: input) + + // THEN + XCTAssertEqual(path, "/\(input.countryCode)/rss/customerreviews/id=\(input.appID)/sortby=mostrecent/json") + } + + func testMakePath_withPartiallyFilledInput() throws { + // GIVEN + input = .init( + appID: .empty, + countryCode: "abc" + ) + + // WHEN + // THEN + XCTAssertThrowsError(try sut.makePath(with: input)) { error in + XCTAssertEqual(error as? EndpointError, .inputParametersEmpty) + } + } + + func testMakePath_withEmptyInput() throws { + // GIVEN + input = .init( + appID: .empty, + countryCode: .empty + ) + + // WHEN + // THEN + XCTAssertThrowsError(try sut.makePath(with: input)) { error in + XCTAssertEqual(error as? EndpointError, .inputParametersEmpty) + } + } + +} + +// MARK: - Array+Constants +private extension Array where Element == Review { + + // MARK: Constants + static let empty: [Review] = [] + + static let many: [Review] = [ + .init( + id: 1, + author: "Some author name #1 goes here...", + title: "Some title #1 goes here...", + content: "Some content #1 goes here...", + rating: 1, + version: "Some version #1 goes here...", + updated: .init() + ), + .init( + id: 2, + author: "Some author name #2 goes here...", + title: "Some title #2 goes here...", + content: "Some content #2 goes here...", + rating: 5, + version: "Some version #2 goes here...", + updated: .init() + ), + .init( + id: 3, + author: "Some author name #3 goes here...", + title: "Some title #3 goes here...", + content: "Some content #3 goes here...", + rating: 3, + version: "Some version #3 goes here...", + updated: .init() + ) + ] + + static let one: [Review] = [ + .init( + id: 1, + author: "Some author name goes here...", + title: "Some title goes here...", + content: "Some content goes here...", + rating: 3, + version: "Some version goes here...", + updated: .init() + ) + ] + +}