From 7c016b50d6b94d927560ecc2f84bb004650ed095 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 17 Mar 2024 22:48:27 +0000 Subject: [PATCH] [Library] iTunes library (#5) This PR contains the work done to implement the `iTunesService` service that fetches the reviews from the **Apple App Store**. Reviewed-on: https://repo.rock-n-code.com/rock-n-code/app-reviews/pulls/5 Co-authored-by: Javier Cicchelli Co-committed-by: Javier Cicchelli --- .../Kit/Sources/Errors/EndpointError.swift | 2 +- .../Feed/Kit/Sources/Models/Review.swift | 19 +++ .../Kit/Sources/Classes/MockURLProtocol.swift | 90 ++++++++++ .../Extensions/JSONDecoder+Constants.swift | 22 +++ .../Extensions/JSONEncoder+Constants.swift | 22 +++ .../Sources/Extensions/URL+Constants.swift | 29 ++++ .../URLSessionConfiguration+Constants.swift | 22 +++ .../Extensions/String+ConstantsTests.swift | 4 +- .../Tests/Extensions/URL+ConstantsTests.swift | 27 +++ .../Kit/Endpoints/GetReviewsAPIEndpoint.swift | 70 ++++++++ .../ServiceConfiguration+Inits.swift | 24 +++ .../iTunes/Kit/Extensions/URL+Constants.swift | 17 ++ Libraries/iTunes/Kit/Models/Feed.swift | 51 ++++++ .../iTunes/Kit/Models/Review+Codable.swift | 84 +++++++++ .../iTunes/Kit/Services/iTunesService.swift | 43 +++++ .../Helpers/Extensions/Array+Reviews.swift | 58 +++++++ .../GetReviewsAPIEndpointTests.swift | 161 ++++++++++++++++++ .../Tests/Services/iTunesServiceTests.swift | 108 ++++++++++++ 18 files changed, 850 insertions(+), 3 deletions(-) create mode 100644 Libraries/Foundation/Kit/Sources/Classes/MockURLProtocol.swift create mode 100644 Libraries/Foundation/Kit/Sources/Extensions/JSONDecoder+Constants.swift create mode 100644 Libraries/Foundation/Kit/Sources/Extensions/JSONEncoder+Constants.swift create mode 100644 Libraries/Foundation/Kit/Sources/Extensions/URL+Constants.swift create mode 100644 Libraries/Foundation/Kit/Sources/Extensions/URLSessionConfiguration+Constants.swift create mode 100644 Libraries/Foundation/Test/Tests/Extensions/URL+ConstantsTests.swift create mode 100644 Libraries/iTunes/Kit/Endpoints/GetReviewsAPIEndpoint.swift create mode 100644 Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift create mode 100644 Libraries/iTunes/Kit/Extensions/URL+Constants.swift create mode 100644 Libraries/iTunes/Kit/Models/Feed.swift create mode 100644 Libraries/iTunes/Kit/Models/Review+Codable.swift create mode 100644 Libraries/iTunes/Kit/Services/iTunesService.swift create mode 100644 Libraries/iTunes/Test/Helpers/Extensions/Array+Reviews.swift create mode 100644 Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift create mode 100644 Libraries/iTunes/Test/Tests/Services/iTunesServiceTests.swift diff --git a/Libraries/Feed/Kit/Sources/Errors/EndpointError.swift b/Libraries/Feed/Kit/Sources/Errors/EndpointError.swift index d8e4249..d19426d 100644 --- a/Libraries/Feed/Kit/Sources/Errors/EndpointError.swift +++ b/Libraries/Feed/Kit/Sources/Errors/EndpointError.swift @@ -6,7 +6,7 @@ // Copyright © 2024 Röck+Cöde VoF. All rights reserved. // -public enum EndpointError: Error { +public enum EndpointError: Error, Equatable { case inputParametersEmpty case requestFailed(statusCode: Int) case responseNotFound diff --git a/Libraries/Feed/Kit/Sources/Models/Review.swift b/Libraries/Feed/Kit/Sources/Models/Review.swift index 90b511e..e1728b3 100644 --- a/Libraries/Feed/Kit/Sources/Models/Review.swift +++ b/Libraries/Feed/Kit/Sources/Models/Review.swift @@ -39,3 +39,22 @@ public struct Review { } } + +// MARK: - Equatable +extension Review: Equatable { + + // MARK: Functions + public static func == ( + lhs: Review, + rhs: Review + ) -> Bool { + lhs.author == rhs.author + && lhs.content == rhs.content + && lhs.id == rhs.id + && lhs.rating == rhs.rating + && lhs.title == rhs.title + && lhs.version == rhs.version + && lhs.updated.description == rhs.updated.description + } + +} diff --git a/Libraries/Foundation/Kit/Sources/Classes/MockURLProtocol.swift b/Libraries/Foundation/Kit/Sources/Classes/MockURLProtocol.swift new file mode 100644 index 0000000..18cdb72 --- /dev/null +++ b/Libraries/Foundation/Kit/Sources/Classes/MockURLProtocol.swift @@ -0,0 +1,90 @@ +// +// MockURLProtocol.swift +// ReviewsFoundationKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +public class MockURLProtocol: URLProtocol { + + // MARK: Properties + public static var response: Response? + + // MARK: Functions + public override class func canInit(with task: URLSessionTask) -> Bool { + true + } + + public override class func canInit(with request: URLRequest) -> Bool { + true + } + + public override class func canonicalRequest(for request: URLRequest) -> URLRequest { + request + } + + public override func startLoading() { + guard + let url = request.url, + let response = Self.response + else { + client?.urlProtocolDidFinishLoading(self) + return + } + + if let data = try? response.data { + client?.urlProtocol(self, didLoad: data) + } + + if let httpResponse = HTTPURLResponse( + url: url, + statusCode: response.statusCode, + httpVersion: nil, + headerFields: nil + ) { + client?.urlProtocol( + self, + didReceive: httpResponse, + cacheStoragePolicy: .allowedInMemoryOnly + ) + } + + client?.urlProtocolDidFinishLoading(self) + } + + public override func stopLoading() {} + +} + +// MARK: - Structs + +extension MockURLProtocol { + public struct Response { + + // MARK: Constants + public let statusCode: Int + public let object: (any Encodable)? + + // MARK: Initialisers + public init( + statusCode: Int, + object: (any Codable)? = nil + ) { + self.statusCode = statusCode + self.object = object + } + + // MARK: Computed + var data: Data? { + get throws { + guard let object else { return nil } + + return try JSONEncoder.default.encode(object) + } + } + + } +} diff --git a/Libraries/Foundation/Kit/Sources/Extensions/JSONDecoder+Constants.swift b/Libraries/Foundation/Kit/Sources/Extensions/JSONDecoder+Constants.swift new file mode 100644 index 0000000..a457872 --- /dev/null +++ b/Libraries/Foundation/Kit/Sources/Extensions/JSONDecoder+Constants.swift @@ -0,0 +1,22 @@ +// +// JSONDecoder+Constants.swift +// ReviewsiTunesKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +extension JSONDecoder { + + // MARK: Constants + public static let `default` = { + let decoder = JSONDecoder() + + decoder.dateDecodingStrategy = .iso8601 + + return decoder + }() + +} diff --git a/Libraries/Foundation/Kit/Sources/Extensions/JSONEncoder+Constants.swift b/Libraries/Foundation/Kit/Sources/Extensions/JSONEncoder+Constants.swift new file mode 100644 index 0000000..ef713ab --- /dev/null +++ b/Libraries/Foundation/Kit/Sources/Extensions/JSONEncoder+Constants.swift @@ -0,0 +1,22 @@ +// +// JSONEncoder+Constants.swift +// ReviewsiTunesKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +extension JSONEncoder { + + // MARK: Constants + public static let `default` = { + let encoder = JSONEncoder() + + encoder.dateEncodingStrategy = .iso8601 + + return encoder + }() + +} diff --git a/Libraries/Foundation/Kit/Sources/Extensions/URL+Constants.swift b/Libraries/Foundation/Kit/Sources/Extensions/URL+Constants.swift new file mode 100644 index 0000000..24bb24c --- /dev/null +++ b/Libraries/Foundation/Kit/Sources/Extensions/URL+Constants.swift @@ -0,0 +1,29 @@ +// +// URL+Constants.swift +// ReviewsFoundationKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +extension URL { + + // MARK: Constants + public static let bitBucket: URL = { + if #available(iOS 16.0, *) { + .init(filePath: .FilePath.bitBucket) + } else { + .init(fileURLWithPath: .FilePath.bitBucket) + } + }() + +} + +// MARK: - String+Constants +private extension String { + enum FilePath { + static let bitBucket = "/dev/null" + } +} diff --git a/Libraries/Foundation/Kit/Sources/Extensions/URLSessionConfiguration+Constants.swift b/Libraries/Foundation/Kit/Sources/Extensions/URLSessionConfiguration+Constants.swift new file mode 100644 index 0000000..aa54f9a --- /dev/null +++ b/Libraries/Foundation/Kit/Sources/Extensions/URLSessionConfiguration+Constants.swift @@ -0,0 +1,22 @@ +// +// URLSessionConfiguration+Constants.swift +// ReviewsFoundationKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation + +extension URLSessionConfiguration { + + // MARK: Constants + public static let mock = { + let configuration: URLSessionConfiguration = .ephemeral + + configuration.protocolClasses = [MockURLProtocol.self] + + return configuration + }() + +} diff --git a/Libraries/Foundation/Test/Tests/Extensions/String+ConstantsTests.swift b/Libraries/Foundation/Test/Tests/Extensions/String+ConstantsTests.swift index fcd80c7..cbbbbd0 100644 --- a/Libraries/Foundation/Test/Tests/Extensions/String+ConstantsTests.swift +++ b/Libraries/Foundation/Test/Tests/Extensions/String+ConstantsTests.swift @@ -6,7 +6,7 @@ // Copyright © 2024 Röck+Cöde VoF. All rights reserved. // -import ReviewsFoundation +import ReviewsFoundationKit import XCTest final class String_ConstantsTests: XCTestCase { @@ -15,7 +15,7 @@ final class String_ConstantsTests: XCTestCase { private var sut: String! // MARK: Constants tests - func testEmpty() throws { + func testEmpty() { // GIVEN sut = .empty diff --git a/Libraries/Foundation/Test/Tests/Extensions/URL+ConstantsTests.swift b/Libraries/Foundation/Test/Tests/Extensions/URL+ConstantsTests.swift new file mode 100644 index 0000000..91167b5 --- /dev/null +++ b/Libraries/Foundation/Test/Tests/Extensions/URL+ConstantsTests.swift @@ -0,0 +1,27 @@ +// +// URL+ConstantsTests.swift +// ReviewsFoundationTest +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation +import ReviewsFoundationKit +import XCTest + +final class URL_ConstantsTests: XCTestCase { + + // MARK: Properties + private var sut: URL! + + // MARK: Constants tests + func testBitBucket() { + sut = .bitBucket + + // WHEN + // THEN + XCTAssertEqual(sut.absoluteString, "file:///dev/null") + } + +} 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/Kit/Extensions/ServiceConfiguration+Inits.swift b/Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift new file mode 100644 index 0000000..f156e71 --- /dev/null +++ b/Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift @@ -0,0 +1,24 @@ +// +// ServiceConfiguration+Inits.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 + +extension ServiceConfiguration { + + // MARK: Initialisers + init(session: URLSessionConfiguration = .ephemeral) { + self.init( + host: .iTunes, + session: session, + decoder: .default + ) + } + +} diff --git a/Libraries/iTunes/Kit/Extensions/URL+Constants.swift b/Libraries/iTunes/Kit/Extensions/URL+Constants.swift new file mode 100644 index 0000000..d989801 --- /dev/null +++ b/Libraries/iTunes/Kit/Extensions/URL+Constants.swift @@ -0,0 +1,17 @@ +// +// URL+Constants.swift +// ReviewsiTunesKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation +import ReviewsFoundationKit + +extension URL { + + // MARK: Constants + static let iTunes: URL = .init(string: "https://itunes.apple.com") ?? .bitBucket + +} diff --git a/Libraries/iTunes/Kit/Models/Feed.swift b/Libraries/iTunes/Kit/Models/Feed.swift new file mode 100644 index 0000000..718004c --- /dev/null +++ b/Libraries/iTunes/Kit/Models/Feed.swift @@ -0,0 +1,51 @@ +// +// Feed.swift +// ReviewsiTunesKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import ReviewsFeedKit + +struct Feed { + + // MARK: Constants + let entries: [Review] + +} + +// MARK: - Decodable +extension Feed: Decodable { + + // MARK: Enumerations + enum FeedKeys: String, CodingKey { + case feed + } + + enum EntryKeys: String, CodingKey { + case entry + } + + // MARK: Initialisers + init(from decoder: any Decoder) throws { + let feed = try decoder.container(keyedBy: FeedKeys.self) + let feedEntry = try feed.nestedContainer(keyedBy: EntryKeys.self, forKey: .feed) + + self.entries = try feedEntry.decode([Review].self, forKey: .entry) + } + +} + +// MARK: - Encodable +extension Feed: Encodable { + + // MARK: Functions + func encode(to encoder: any Encoder) throws { + var feed = encoder.container(keyedBy: FeedKeys.self) + var feedEntry = feed.nestedContainer(keyedBy: EntryKeys.self, forKey: .feed) + + try feedEntry.encode(entries, forKey: .entry) + } + +} diff --git a/Libraries/iTunes/Kit/Models/Review+Codable.swift b/Libraries/iTunes/Kit/Models/Review+Codable.swift new file mode 100644 index 0000000..c60640a --- /dev/null +++ b/Libraries/iTunes/Kit/Models/Review+Codable.swift @@ -0,0 +1,84 @@ +// +// Review+Codable.swift +// ReviewsiTunesKit +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation +import ReviewsFeedKit + +// MARK: - Decodable +extension Review: Decodable { + + // MARK: Enumerations + enum ReviewKeys: String, CodingKey { + case author + case content + case id + case rating = "im:rating" + case title + case updated + case version = "im:version" + } + + enum NameKeys: String, CodingKey { + case name + } + + enum LabelKeys: String, CodingKey { + case label + } + + // MARK: Initialisers + public init(from decoder: any Decoder) throws { + let review = try decoder.container(keyedBy: ReviewKeys.self) + let authorName = try review.nestedContainer(keyedBy: NameKeys.self, forKey: .author) + let authorLabel = try authorName.nestedContainer(keyedBy: LabelKeys.self, forKey: .name) + let contentLabel = try review.nestedContainer(keyedBy: LabelKeys.self, forKey: .content) + let idLabel = try review.nestedContainer(keyedBy: LabelKeys.self, forKey: .id) + let ratingLabel = try review.nestedContainer(keyedBy: LabelKeys.self, forKey: .rating) + let titleLabel = try review.nestedContainer(keyedBy: LabelKeys.self, forKey: .title) + let versionLabel = try review.nestedContainer(keyedBy: LabelKeys.self, forKey: .version) + let updatedLabel = try review.nestedContainer(keyedBy: LabelKeys.self, forKey: .updated) + + self.init( + id: Int(try idLabel.decode(String.self, forKey: .label)) ?? 0, + author: try authorLabel.decode(String.self, forKey: .label), + title: try titleLabel.decode(String.self, forKey: .label), + content: try contentLabel.decode(String.self, forKey: .label), + rating: Int(try ratingLabel.decode(String.self, forKey: .label)) ?? 0, + version: try versionLabel.decode(String.self, forKey: .label), + updated: try updatedLabel.decode(Date.self, forKey: .label) + ) + } + +} + +// MARK: - Encodable +extension Review: Encodable { + + // MARK: Functions + + public func encode(to encoder: any Encoder) throws { + var review = encoder.container(keyedBy: ReviewKeys.self) + var authorName = review.nestedContainer(keyedBy: NameKeys.self, forKey: .author) + var authorLabel = authorName.nestedContainer(keyedBy: LabelKeys.self, forKey: .name) + var contentLabel = review.nestedContainer(keyedBy: LabelKeys.self, forKey: .content) + var idLabel = review.nestedContainer(keyedBy: LabelKeys.self, forKey: .id) + var ratingLabel = review.nestedContainer(keyedBy: LabelKeys.self, forKey: .rating) + var titleLabel = review.nestedContainer(keyedBy: LabelKeys.self, forKey: .title) + var versionLabel = review.nestedContainer(keyedBy: LabelKeys.self, forKey: .version) + var updatedLabel = review.nestedContainer(keyedBy: LabelKeys.self, forKey: .updated) + + try authorLabel.encode(author, forKey: .label) + try contentLabel.encode(content, forKey: .label) + try idLabel.encode(String(id), forKey: .label) + try ratingLabel.encode(String(rating), forKey: .label) + try titleLabel.encode(title, forKey: .label) + try versionLabel.encode(version, forKey: .label) + try updatedLabel.encode(updated, forKey: .label) + } + +} diff --git a/Libraries/iTunes/Kit/Services/iTunesService.swift b/Libraries/iTunes/Kit/Services/iTunesService.swift new file mode 100644 index 0000000..09f5c66 --- /dev/null +++ b/Libraries/iTunes/Kit/Services/iTunesService.swift @@ -0,0 +1,43 @@ +// +// iTunesService.swift +// ReviewsiTunes +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import Foundation +import ReviewsFeedKit + +public actor iTunesService: Service { + + // MARK: Constants + public let configuration: ServiceConfiguration + public let decoder: JSONDecoder + public let session: URLSession + + // MARK: Properties + private lazy var getReviewsEndpoint: GetReviewsAPIEndpoint = { + .init( + host: configuration.host, + decoder: decoder, + session: session + ) + }() + + // MARK: Initialisers + public init(configuration: ServiceConfiguration) { + self.configuration = configuration + self.decoder = configuration.decoder + self.session = .init(configuration: configuration.session) + } + + // MARK: Functions + @discardableResult public func getReviews( + _ input: GetReviewsInput + ) async throws -> GetReviewsOutput { + try await getReviewsEndpoint(input) + } + +} + diff --git a/Libraries/iTunes/Test/Helpers/Extensions/Array+Reviews.swift b/Libraries/iTunes/Test/Helpers/Extensions/Array+Reviews.swift new file mode 100644 index 0000000..5a0a419 --- /dev/null +++ b/Libraries/iTunes/Test/Helpers/Extensions/Array+Reviews.swift @@ -0,0 +1,58 @@ +// +// Array+Reviews.swift +// ReviewsiTunesTest +// +// Created by Javier Cicchelli on 17/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import ReviewsFeedKit + +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() + ) + ] + +} diff --git a/Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift b/Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift new file mode 100644 index 0000000..a34ab03 --- /dev/null +++ b/Libraries/iTunes/Test/Tests/Endpoints/GetReviewsAPIEndpointTests.swift @@ -0,0 +1,161 @@ +// +// 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, 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, 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, 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) + } + } + +} diff --git a/Libraries/iTunes/Test/Tests/Services/iTunesServiceTests.swift b/Libraries/iTunes/Test/Tests/Services/iTunesServiceTests.swift new file mode 100644 index 0000000..c52ec9b --- /dev/null +++ b/Libraries/iTunes/Test/Tests/Services/iTunesServiceTests.swift @@ -0,0 +1,108 @@ +// +// iTunesServiceTests.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 iTunesServiceTests: XCTestCase { + + // MARK: Properties + private var sut: iTunesService! + + // MARK: Setup + override func setUp() async throws { + sut = .init(configuration: .init(session: .mock)) + } + + // MARK: Functions + func testGetReviews_whenResponseOK_withOneReview() async throws { + // GIVEN + let reviews: [Review] = .one + + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: reviews) + ) + + // WHEN + let output = try await sut.getReviews(.init( + appID: "1234567890", + countryCode: "abc" + )) + + // THEN + XCTAssertFalse(output.reviews.isEmpty) + XCTAssertEqual(output.reviews.count, reviews.count) + XCTAssertEqual(output.reviews, reviews) + } + + func testGetReviews_whenResponseOK_withManyReview() async throws { + // GIVEN + let reviews: [Review] = .many + + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: reviews) + ) + + // WHEN + let output = try await sut.getReviews(.init( + appID: "1234567890", + countryCode: "abc" + )) + + // THEN + XCTAssertFalse(output.reviews.isEmpty) + XCTAssertEqual(output.reviews.count, reviews.count) + XCTAssertEqual(output.reviews, reviews) + } + + func testGetReviews_whenResponseOK_withEmptyReview() async throws { + // GIVEN + let reviews: [Review] = .empty + + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: reviews) + ) + + // WHEN + let output = try await sut.getReviews(.init( + appID: "1234567890", + countryCode: "abc" + )) + + // THEN + XCTAssertTrue(output.reviews.isEmpty) + XCTAssertEqual(output.reviews, reviews) + } + + func testGetReviews_whenResponseNotOK() async throws { + // GIVEN + let statusCode = 404 + + MockURLProtocol.response = .init(statusCode: statusCode) + + // WHEN + // THEN + do { + try await sut.getReviews(.init( + appID: "1234567890", + countryCode: "abc" + )) + } catch EndpointError.requestFailed(statusCode: statusCode) { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + +}