diff --git a/Sources/TMDb/Authentication/AuthenticationService.swift b/Sources/TMDb/Authentication/AuthenticationService.swift deleted file mode 100644 index b5192f90..00000000 --- a/Sources/TMDb/Authentication/AuthenticationService.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// AuthenticationService.swift -// TMDb -// -// Copyright © 2024 Adam Young. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an AS IS BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -/// -/// Provides an interface for authenticating with TMDb. -/// -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public final class AuthenticationService { - - private let apiClient: any APIClient - - /// - /// Creates an authentication service object. - /// - public convenience init() { - self.init( - apiClient: TMDbFactory.authAPIClient - ) - } - - init(apiClient: some APIClient) { - self.apiClient = apiClient - } - - /// - /// Creates a guest session with TMDb. - /// - /// Guest sessions are a special kind of session that give you some of the - /// functionality of an account, but not all. For example, some of the - /// things you can do with a guest session are; maintain a rated list, a - /// watchlist and a favourite list. - /// - /// Guest sessions will automatically be deleted if they are not used - /// within 60 minutes of it being issued. - /// - /// [TMDb API - Authentication: Create Guest Session](https://developer.themoviedb.org/reference/certifications-tv-list) - /// - /// - Throws: TMDb error ``TMDbError``. - /// - /// - Returns: A guest session. - /// - public func createGuestSession() async throws -> GuestSession { - let session: GuestSession - do { - session = try await apiClient.get(endpoint: AuthenticationEndpoint.createGuestSession) - } catch let error { - throw TMDbError(error: error) - } - - return session - } - -} diff --git a/Sources/TMDb/Extensions/URL+TMDb.swift b/Sources/TMDb/Extensions/URL+TMDb.swift index ad2e94eb..21c7daa3 100644 --- a/Sources/TMDb/Extensions/URL+TMDb.swift +++ b/Sources/TMDb/Extensions/URL+TMDb.swift @@ -25,4 +25,8 @@ extension URL { URL(string: "https://api.themoviedb.org/3")! } + static var tmdbWebSiteURL: URL { + URL(string: "https://www.themoviedb.org")! + } + } diff --git a/Sources/TMDb/Helpers/AuthenticateURLBuilder.swift b/Sources/TMDb/Helpers/AuthenticateURLBuilder.swift new file mode 100644 index 00000000..72f153d2 --- /dev/null +++ b/Sources/TMDb/Helpers/AuthenticateURLBuilder.swift @@ -0,0 +1,46 @@ +// +// AuthenticateURLBuilder.swift +// TMDb +// +// Copyright © 2024 Adam Young. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an AS IS BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +final class AuthenticateURLBuilder: AuthenticateURLBuilding { + + private let baseURL: URL + + init(baseURL: URL) { + self.baseURL = baseURL + } + + func authenticateURL(with requestToken: String) -> URL { + authenticateURL(with: requestToken, redirectURL: nil) + } + + func authenticateURL(with requestToken: String, redirectURL: URL?) -> URL { + var url = baseURL + .appendingPathComponent("authenticate") + .appendingPathComponent(requestToken) + + if let redirectURL { + url = url.appendingQueryItem(name: "redirect_to", value: redirectURL.absoluteString) + } + + return url + } + +} diff --git a/Sources/TMDb/Models/Token.swift b/Sources/TMDb/Models/Token.swift new file mode 100644 index 00000000..01ce5c14 --- /dev/null +++ b/Sources/TMDb/Models/Token.swift @@ -0,0 +1,56 @@ +// +// Token.swift +// TMDb +// +// Copyright © 2024 Adam Young. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an AS IS BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// +/// A model representing an internediate request token. +/// +public struct Token: Codable, Equatable, Hashable { + + /// + /// Was token creation successful. + /// + public let success: Bool + + /// + /// An intermediate request token. + /// + public let requestToken: String + + /// + /// Date of token expiry. + /// + public let expiresAt: Date + + /// + /// Creates an internediate request token. + /// + /// - Parameters: + /// - success: Was token creation successful. + /// - requestToken: An intermediate request token. + /// - expiresAt: Date of token expiry. + /// + public init(success: Bool, requestToken: String, expiresAt: Date) { + self.success = success + self.requestToken = requestToken + self.expiresAt = expiresAt + } + +} diff --git a/Sources/TMDb/Networking/HTTPClient/HTTPClient.swift b/Sources/TMDb/Networking/APIClient/HTTPClient.swift similarity index 100% rename from Sources/TMDb/Networking/HTTPClient/HTTPClient.swift rename to Sources/TMDb/Networking/APIClient/HTTPClient.swift diff --git a/Sources/TMDb/Networking/APIClient/APIClient.swift b/Sources/TMDb/Services/APIClient.swift similarity index 100% rename from Sources/TMDb/Networking/APIClient/APIClient.swift rename to Sources/TMDb/Services/APIClient.swift diff --git a/Sources/TMDb/Services/Authentication/AuthenticateURLBuilding.swift b/Sources/TMDb/Services/Authentication/AuthenticateURLBuilding.swift new file mode 100644 index 00000000..a05c8d46 --- /dev/null +++ b/Sources/TMDb/Services/Authentication/AuthenticateURLBuilding.swift @@ -0,0 +1,28 @@ +// +// AuthenticateURLBuilding.swift +// TMDb +// +// Copyright © 2024 Adam Young. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an AS IS BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol AuthenticateURLBuilding { + + func authenticateURL(with requestToken: String) -> URL + + func authenticateURL(with requestToken: String, redirectURL: URL?) -> URL + +} diff --git a/Sources/TMDb/Authentication/Endpoints/AuthenticationEndpoint.swift b/Sources/TMDb/Services/Authentication/AuthenticationEndpoint.swift similarity index 85% rename from Sources/TMDb/Authentication/Endpoints/AuthenticationEndpoint.swift rename to Sources/TMDb/Services/Authentication/AuthenticationEndpoint.swift index 5c7a318e..23c2a4dc 100644 --- a/Sources/TMDb/Authentication/Endpoints/AuthenticationEndpoint.swift +++ b/Sources/TMDb/Services/Authentication/AuthenticationEndpoint.swift @@ -22,6 +22,7 @@ import Foundation enum AuthenticationEndpoint { case createGuestSession + case createRequestToken } @@ -35,6 +36,11 @@ extension AuthenticationEndpoint: Endpoint { Self.basePath .appendingPathComponent("guest_session") .appendingPathComponent("new") + + case .createRequestToken: + Self.basePath + .appendingPathComponent("token") + .appendingPathComponent("new") } } diff --git a/Sources/TMDb/Services/Authentication/AuthenticationService.swift b/Sources/TMDb/Services/Authentication/AuthenticationService.swift new file mode 100644 index 00000000..03308b18 --- /dev/null +++ b/Sources/TMDb/Services/Authentication/AuthenticationService.swift @@ -0,0 +1,116 @@ +// +// AuthenticationService.swift +// TMDb +// +// Copyright © 2024 Adam Young. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an AS IS BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// +/// Provides an interface for authenticating and generating session IDs with TMDb. +/// +/// Details of generating session IDs for TMDb can be found at +/// [TMDb API - How do I generate a session ID?](https://developer.themoviedb.org/reference/authentication-how-do-i-generate-a-session-id) +/// +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public final class AuthenticationService { + + private let apiClient: any APIClient + private let authenticateURLBuilder: any AuthenticateURLBuilding + + /// + /// Creates an authentication service object. + /// + public convenience init() { + self.init( + apiClient: TMDbFactory.authAPIClient, + authenticateURLBuilder: TMDbFactory.authenticateURLBuilder + ) + } + + init( + apiClient: some APIClient, + authenticateURLBuilder: some AuthenticateURLBuilding + ) { + self.apiClient = apiClient + self.authenticateURLBuilder = authenticateURLBuilder + } + + /// + /// Creates a guest session with TMDb. + /// + /// Guest sessions are a special kind of session that give you some of the functionality of an account, but not + /// all. For example, some of the things you can do with a guest session are; maintain a rated list, a watchlist + /// and a favourite list. + /// + /// Guest sessions will automatically be deleted if they are not used within 60 minutes of it being issued. + /// + /// [TMDb API - Authentication: Create Guest Session](https://developer.themoviedb.org/reference/authentication-create-guest-session) + /// + /// - Throws: TMDb error ``TMDbError``. + /// + /// - Returns: A guest session. + /// + public func guestSession() async throws -> GuestSession { + let session: GuestSession + do { + session = try await apiClient.get(endpoint: AuthenticationEndpoint.createGuestSession) + } catch let error { + throw TMDbError(error: error) + } + + return session + } + + /// + /// Creates an intermediate request token that can be used to validate a TMDb user login. + /// + /// This is a temporary token that is required to ask the user for permission to access their account. This token + /// will auto expire after 60 minutes if it's not used. + /// + /// [TMDb API - Authentication: Create Request Token](https://developer.themoviedb.org/reference/authentication-create-request-token) + /// + /// - Returns: An intermediate request token. + /// + public func requestToken() async throws -> Token { + let token: Token + do { + token = try await apiClient.get(endpoint: AuthenticationEndpoint.createRequestToken) + } catch let error { + throw TMDbError(error: error) + } + + return token + } + + /// + /// Builds the URL used for the user to authenticate with after requesting an intermediate request token. + /// + /// An internediate request token can be generated by calling ``requestToken()``. + /// + /// - Parameters: + /// - token: An intermediate request token. + /// - redirectURL: Optional URL to redirect to once the user has authenticated. + /// + /// - Returns: An authenticate URL. + /// + public func authenticateURL(for token: Token, redirectURL: URL? = nil) -> URL { + let url = authenticateURLBuilder.authenticateURL(with: token.requestToken, redirectURL: redirectURL) + + return url + } + +} diff --git a/Sources/TMDb/Certifications/CertificationService.swift b/Sources/TMDb/Services/Certifications/CertificationService.swift similarity index 100% rename from Sources/TMDb/Certifications/CertificationService.swift rename to Sources/TMDb/Services/Certifications/CertificationService.swift diff --git a/Sources/TMDb/Certifications/Endpoints/CertificationsEndpoint.swift b/Sources/TMDb/Services/Certifications/CertificationsEndpoint.swift similarity index 100% rename from Sources/TMDb/Certifications/Endpoints/CertificationsEndpoint.swift rename to Sources/TMDb/Services/Certifications/CertificationsEndpoint.swift diff --git a/Sources/TMDb/Company/Endpoints/CompanyEndpoint.swift b/Sources/TMDb/Services/Company/CompanyEndpoint.swift similarity index 100% rename from Sources/TMDb/Company/Endpoints/CompanyEndpoint.swift rename to Sources/TMDb/Services/Company/CompanyEndpoint.swift diff --git a/Sources/TMDb/Company/CompanyService.swift b/Sources/TMDb/Services/Company/CompanyService.swift similarity index 100% rename from Sources/TMDb/Company/CompanyService.swift rename to Sources/TMDb/Services/Company/CompanyService.swift diff --git a/Sources/TMDb/Configuration/Endpoint/ConfigurationEndpoint.swift b/Sources/TMDb/Services/Configuration/ConfigurationEndpoint.swift similarity index 100% rename from Sources/TMDb/Configuration/Endpoint/ConfigurationEndpoint.swift rename to Sources/TMDb/Services/Configuration/ConfigurationEndpoint.swift diff --git a/Sources/TMDb/Configuration/ConfigurationService.swift b/Sources/TMDb/Services/Configuration/ConfigurationService.swift similarity index 100% rename from Sources/TMDb/Configuration/ConfigurationService.swift rename to Sources/TMDb/Services/Configuration/ConfigurationService.swift diff --git a/Sources/TMDb/Discover/Endpoints/DiscoverEndpoint.swift b/Sources/TMDb/Services/Discover/DiscoverEndpoint.swift similarity index 100% rename from Sources/TMDb/Discover/Endpoints/DiscoverEndpoint.swift rename to Sources/TMDb/Services/Discover/DiscoverEndpoint.swift diff --git a/Sources/TMDb/Discover/DiscoverService.swift b/Sources/TMDb/Services/Discover/DiscoverService.swift similarity index 100% rename from Sources/TMDb/Discover/DiscoverService.swift rename to Sources/TMDb/Services/Discover/DiscoverService.swift diff --git a/Sources/TMDb/Genres/GenreService.swift b/Sources/TMDb/Services/Genres/GenreService.swift similarity index 100% rename from Sources/TMDb/Genres/GenreService.swift rename to Sources/TMDb/Services/Genres/GenreService.swift diff --git a/Sources/TMDb/Genres/Endpoints/GenresEndpoint.swift b/Sources/TMDb/Services/Genres/GenresEndpoint.swift similarity index 100% rename from Sources/TMDb/Genres/Endpoints/GenresEndpoint.swift rename to Sources/TMDb/Services/Genres/GenresEndpoint.swift diff --git a/Sources/TMDb/LocaleProviding.swift b/Sources/TMDb/Services/LocaleProviding.swift similarity index 100% rename from Sources/TMDb/LocaleProviding.swift rename to Sources/TMDb/Services/LocaleProviding.swift diff --git a/Sources/TMDb/Movies/MovieService.swift b/Sources/TMDb/Services/Movies/MovieService.swift similarity index 99% rename from Sources/TMDb/Movies/MovieService.swift rename to Sources/TMDb/Services/Movies/MovieService.swift index 962a7fb9..5dd8d032 100644 --- a/Sources/TMDb/Movies/MovieService.swift +++ b/Sources/TMDb/Services/Movies/MovieService.swift @@ -325,6 +325,7 @@ public final class MovieService { /// Returns watch providers for a movie /// /// [TMDb API - Movie: Watch providers](https://developer.themoviedb.org/reference/movie-watch-providers) + /// /// - Parameters: /// - id: The identifier of the movie. /// diff --git a/Sources/TMDb/Movies/Endpoints/MoviesEndpoint.swift b/Sources/TMDb/Services/Movies/MoviesEndpoint.swift similarity index 100% rename from Sources/TMDb/Movies/Endpoints/MoviesEndpoint.swift rename to Sources/TMDb/Services/Movies/MoviesEndpoint.swift diff --git a/Sources/TMDb/People/Endpoints/PeopleEndpoint.swift b/Sources/TMDb/Services/People/PeopleEndpoint.swift similarity index 100% rename from Sources/TMDb/People/Endpoints/PeopleEndpoint.swift rename to Sources/TMDb/Services/People/PeopleEndpoint.swift diff --git a/Sources/TMDb/People/PersonService.swift b/Sources/TMDb/Services/People/PersonService.swift similarity index 100% rename from Sources/TMDb/People/PersonService.swift rename to Sources/TMDb/Services/People/PersonService.swift diff --git a/Sources/TMDb/Search/Endpoints/SearchEndpoint.swift b/Sources/TMDb/Services/Search/SearchEndpoint.swift similarity index 100% rename from Sources/TMDb/Search/Endpoints/SearchEndpoint.swift rename to Sources/TMDb/Services/Search/SearchEndpoint.swift diff --git a/Sources/TMDb/Search/SearchService.swift b/Sources/TMDb/Services/Search/SearchService.swift similarity index 100% rename from Sources/TMDb/Search/SearchService.swift rename to Sources/TMDb/Services/Search/SearchService.swift diff --git a/Sources/TMDb/TVEpisodes/TVEpisodeService.swift b/Sources/TMDb/Services/TVEpisodes/TVEpisodeService.swift similarity index 100% rename from Sources/TMDb/TVEpisodes/TVEpisodeService.swift rename to Sources/TMDb/Services/TVEpisodes/TVEpisodeService.swift diff --git a/Sources/TMDb/TVEpisodes/Endpoints/TVEpisodesEndpoint.swift b/Sources/TMDb/Services/TVEpisodes/TVEpisodesEndpoint.swift similarity index 100% rename from Sources/TMDb/TVEpisodes/Endpoints/TVEpisodesEndpoint.swift rename to Sources/TMDb/Services/TVEpisodes/TVEpisodesEndpoint.swift diff --git a/Sources/TMDb/TVSeasons/TVSeasonService.swift b/Sources/TMDb/Services/TVSeasons/TVSeasonService.swift similarity index 100% rename from Sources/TMDb/TVSeasons/TVSeasonService.swift rename to Sources/TMDb/Services/TVSeasons/TVSeasonService.swift diff --git a/Sources/TMDb/TVSeasons/Endpoints/TVSeasonsEndpoint.swift b/Sources/TMDb/Services/TVSeasons/TVSeasonsEndpoint.swift similarity index 100% rename from Sources/TMDb/TVSeasons/Endpoints/TVSeasonsEndpoint.swift rename to Sources/TMDb/Services/TVSeasons/TVSeasonsEndpoint.swift diff --git a/Sources/TMDb/TVSeries/Endpoints/TVSeriesEndpoint.swift b/Sources/TMDb/Services/TVSeries/TVSeriesEndpoint.swift similarity index 100% rename from Sources/TMDb/TVSeries/Endpoints/TVSeriesEndpoint.swift rename to Sources/TMDb/Services/TVSeries/TVSeriesEndpoint.swift diff --git a/Sources/TMDb/TVSeries/TVSeriesService.swift b/Sources/TMDb/Services/TVSeries/TVSeriesService.swift similarity index 100% rename from Sources/TMDb/TVSeries/TVSeriesService.swift rename to Sources/TMDb/Services/TVSeries/TVSeriesService.swift diff --git a/Sources/TMDb/Trending/Endpoints/TrendingEndpoint.swift b/Sources/TMDb/Services/Trending/TrendingEndpoint.swift similarity index 100% rename from Sources/TMDb/Trending/Endpoints/TrendingEndpoint.swift rename to Sources/TMDb/Services/Trending/TrendingEndpoint.swift diff --git a/Sources/TMDb/Trending/TrendingService.swift b/Sources/TMDb/Services/Trending/TrendingService.swift similarity index 100% rename from Sources/TMDb/Trending/TrendingService.swift rename to Sources/TMDb/Services/Trending/TrendingService.swift diff --git a/Sources/TMDb/Trending/TrendingTimeWindowFilterType.swift b/Sources/TMDb/Services/Trending/TrendingTimeWindowFilterType.swift similarity index 100% rename from Sources/TMDb/Trending/TrendingTimeWindowFilterType.swift rename to Sources/TMDb/Services/Trending/TrendingTimeWindowFilterType.swift diff --git a/Sources/TMDb/WatchProviders/Endpoints/WatchProviderEndpoint.swift b/Sources/TMDb/Services/WatchProviders/WatchProviderEndpoint.swift similarity index 100% rename from Sources/TMDb/WatchProviders/Endpoints/WatchProviderEndpoint.swift rename to Sources/TMDb/Services/WatchProviders/WatchProviderEndpoint.swift diff --git a/Sources/TMDb/WatchProviders/WatchProviderService.swift b/Sources/TMDb/Services/WatchProviders/WatchProviderService.swift similarity index 100% rename from Sources/TMDb/WatchProviders/WatchProviderService.swift rename to Sources/TMDb/Services/WatchProviders/WatchProviderService.swift diff --git a/Sources/TMDb/TMDb.docc/Extensions/AuthenticationService.md b/Sources/TMDb/TMDb.docc/Extensions/AuthenticationService.md index 7596d4c4..c8f9d72f 100644 --- a/Sources/TMDb/TMDb.docc/Extensions/AuthenticationService.md +++ b/Sources/TMDb/TMDb.docc/Extensions/AuthenticationService.md @@ -8,4 +8,6 @@ ### Creating Sessions -- ``createGuestSession()`` +- ``guestSession()`` +- ``requestToken()`` +- ``authenticateURL(for:redirectURL:)`` diff --git a/Sources/TMDb/TMDbFactory.swift b/Sources/TMDb/TMDbFactory.swift index 567eabdc..f4f497e1 100644 --- a/Sources/TMDb/TMDbFactory.swift +++ b/Sources/TMDb/TMDbFactory.swift @@ -32,9 +32,9 @@ extension TMDbFactory { static var apiClient: some APIClient { TMDbAPIClient( - apiKey: TMDb.configuration.apiKey(), - baseURL: .tmdbAPIBaseURL, - httpClient: TMDb.configuration.httpClient(), + apiKey: apiKey, + baseURL: tmdbAPIBaseURL, + httpClient: httpClient, serialiser: serialiser, localeProvider: localeProvider() ) @@ -42,14 +42,18 @@ extension TMDbFactory { static var authAPIClient: some APIClient { TMDbAPIClient( - apiKey: TMDb.configuration.apiKey(), + apiKey: apiKey, baseURL: .tmdbAPIBaseURL, - httpClient: TMDb.configuration.httpClient(), + httpClient: httpClient, serialiser: authSerialiser, localeProvider: localeProvider() ) } + static var authenticateURLBuilder: some AuthenticateURLBuilding { + AuthenticateURLBuilder(baseURL: tmdbWebSiteURL) + } + static func localeProvider() -> some LocaleProviding { LocaleProvider(locale: .current) } @@ -96,3 +100,23 @@ extension TMDbFactory { } } + +extension TMDbFactory { + + private static var tmdbAPIBaseURL: URL { + URL.tmdbAPIBaseURL + } + + private static var tmdbWebSiteURL: URL { + URL.tmdbWebSiteURL + } + + private static var apiKey: String { + TMDb.configuration.apiKey() + } + + private static var httpClient: any HTTPClient { + TMDb.configuration.httpClient() + } + +} diff --git a/Tests/TMDbIntegrationTests/AuthenticationIntegrationTests.swift b/Tests/TMDbIntegrationTests/AuthenticationIntegrationTests.swift index 6b6a105d..c145cc14 100644 --- a/Tests/TMDbIntegrationTests/AuthenticationIntegrationTests.swift +++ b/Tests/TMDbIntegrationTests/AuthenticationIntegrationTests.swift @@ -35,11 +35,18 @@ final class AuthenticationIntegrationTests: XCTestCase { super.tearDown() } - func testCreateGuestSession() async throws { - let session = try await authenticationService.createGuestSession() + func testGuestSession() async throws { + let session = try await authenticationService.guestSession() XCTAssertTrue(session.success) XCTAssertNotEqual(session.guestSessionID, "") } + func testRequestToken() async throws { + let token = try await authenticationService.requestToken() + + XCTAssertTrue(token.success) + XCTAssertNotEqual(token.requestToken, "") + } + } diff --git a/Tests/TMDbTests/Authentication/AuthenticationServiceTests.swift b/Tests/TMDbTests/Authentication/AuthenticationServiceTests.swift deleted file mode 100644 index 3fd25bb6..00000000 --- a/Tests/TMDbTests/Authentication/AuthenticationServiceTests.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// AuthenticationServiceTests.swift -// TMDb -// -// Copyright © 2024 Adam Young. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an AS IS BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -@testable import TMDb -import XCTest - -final class AuthenticationServiceTests: XCTestCase { - - var service: AuthenticationService! - var apiClient: MockAPIClient! - - override func setUp() { - super.setUp() - apiClient = MockAPIClient() - service = AuthenticationService(apiClient: apiClient) - } - - override func tearDown() { - apiClient = nil - service = nil - super.tearDown() - } - - func testCreateGuestSessionReturnsGuestSession() async throws { - let expectedResult = GuestSession.mock(expiresAt: Date(timeIntervalSince1970: 1_705_956_596)) - - apiClient.result = .success(expectedResult) - - let result = try await service.createGuestSession() - - XCTAssertEqual(result, expectedResult) - XCTAssertEqual(apiClient.lastPath, AuthenticationEndpoint.createGuestSession.path) - } - - func testCreateGuestSessionWhenErrorsThrowsError() async throws { - apiClient.result = .failure(.unknown) - - var error: Error? - do { - _ = try await service.createGuestSession() - } catch let err { - error = err - } - - let tmdbAPIError = try XCTUnwrap(error as? TMDbError) - - XCTAssertEqual(tmdbAPIError, .unknown) - } - -} diff --git a/Tests/TMDbTests/Helpers/AuthenticateURLBuilderTests.swift b/Tests/TMDbTests/Helpers/AuthenticateURLBuilderTests.swift new file mode 100644 index 00000000..1f88657e --- /dev/null +++ b/Tests/TMDbTests/Helpers/AuthenticateURLBuilderTests.swift @@ -0,0 +1,64 @@ +// +// AuthenticateURLBuilderTests.swift +// TMDb +// +// Copyright © 2024 Adam Young. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an AS IS BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@testable import TMDb +import XCTest + +final class AuthenticateURLBuilderTests: XCTestCase { + + var builder: AuthenticateURLBuilder! + var baseURL: URL! + + override func setUp() { + super.setUp() + baseURL = URL(string: "https://some.domain.com") + builder = AuthenticateURLBuilder(baseURL: baseURL) + } + + override func tearDown() { + builder = nil + baseURL = nil + super.tearDown() + } + + func testAuthenticateURLReturnsURL() { + let requestToken = "qwertyuiop" + let expectedURL = baseURL + .appendingPathComponent("authenticate") + .appendingPathComponent(requestToken) + + let url = builder.authenticateURL(with: requestToken) + + XCTAssertEqual(url, expectedURL) + } + + func testAuthenticateURLWithRedirectURLReturnsURL() throws { + let requestToken = "qwertyuiop" + let redirectURL = try XCTUnwrap(URL(string: "https://my.domain.com/auth/callback")) + let expectedURL = baseURL + .appendingPathComponent("authenticate") + .appendingPathComponent(requestToken) + .appendingQueryItem(name: "redirect_to", value: redirectURL.absoluteString) + + let url = builder.authenticateURL(with: requestToken, redirectURL: redirectURL) + + XCTAssertEqual(url, expectedURL) + } + +} diff --git a/Tests/TMDbTests/Mocks/Authentication/AuthenticateURLMockBuilder.swift b/Tests/TMDbTests/Mocks/Authentication/AuthenticateURLMockBuilder.swift new file mode 100644 index 00000000..de8be9ff --- /dev/null +++ b/Tests/TMDbTests/Mocks/Authentication/AuthenticateURLMockBuilder.swift @@ -0,0 +1,40 @@ +// +// AuthenticateURLMockBuilder.swift +// TMDb +// +// Copyright © 2024 Adam Young. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an AS IS BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import TMDb + +final class AuthenticateURLMockBuilder: AuthenticateURLBuilding { + + var authenticateURLResult: URL = .init(string: "https://some.domain.com/authenticate")! + private(set) var lastRequestToken: String? + private(set) var lastRedirectURL: URL? + + func authenticateURL(with requestToken: String) -> URL { + authenticateURL(with: requestToken, redirectURL: nil) + } + + func authenticateURL(with requestToken: String, redirectURL: URL?) -> URL { + lastRequestToken = requestToken + lastRedirectURL = redirectURL + + return authenticateURLResult + } + +} diff --git a/Tests/TMDbTests/Mocks/LocaleMockProvider.swift b/Tests/TMDbTests/Mocks/Helpers/LocaleMockProvider.swift similarity index 100% rename from Tests/TMDbTests/Mocks/LocaleMockProvider.swift rename to Tests/TMDbTests/Mocks/Helpers/LocaleMockProvider.swift diff --git a/Tests/TMDbTests/Mocks/Models/GuestSession+Mocks.swift b/Tests/TMDbTests/Mocks/Models/GuestSession+Mocks.swift index 4c8de781..59729aae 100644 --- a/Tests/TMDbTests/Mocks/Models/GuestSession+Mocks.swift +++ b/Tests/TMDbTests/Mocks/Models/GuestSession+Mocks.swift @@ -25,7 +25,7 @@ extension GuestSession { static func mock( success: Bool = true, guestSessionID: String = "jdbqej40d9b562zk42ma8u4tp1saup5q", - expiresAt: Date + expiresAt: Date = Date(timeIntervalSince1970: 1_705_956_596) ) -> GuestSession { GuestSession( success: success, diff --git a/Tests/TMDbTests/Mocks/Models/Token+Mocks.swift b/Tests/TMDbTests/Mocks/Models/Token+Mocks.swift new file mode 100644 index 00000000..a0211142 --- /dev/null +++ b/Tests/TMDbTests/Mocks/Models/Token+Mocks.swift @@ -0,0 +1,37 @@ +// +// Token+Mocks.swift +// TMDb +// +// Copyright © 2024 Adam Young. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an AS IS BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import TMDb + +extension Token { + + static func mock( + success: Bool = true, + requestToken: String = "10530f2246e244555d122016db7c65599c8d6f4d", + expiresAt: Date = Date(timeIntervalSince1970: 1_705_956_596) + ) -> Token { + Token( + success: success, + requestToken: requestToken, + expiresAt: expiresAt + ) + } + +} diff --git a/Tests/TMDbTests/Mocks/Client/MockAPIClient.swift b/Tests/TMDbTests/Mocks/Networking/APIClient/MockAPIClient.swift similarity index 100% rename from Tests/TMDbTests/Mocks/Client/MockAPIClient.swift rename to Tests/TMDbTests/Mocks/Networking/APIClient/MockAPIClient.swift diff --git a/Tests/TMDbTests/Networking/Mocks/HTTPMockClient.swift b/Tests/TMDbTests/Mocks/Networking/HTTPClient/HTTPMockClient.swift similarity index 100% rename from Tests/TMDbTests/Networking/Mocks/HTTPMockClient.swift rename to Tests/TMDbTests/Mocks/Networking/HTTPClient/HTTPMockClient.swift diff --git a/Tests/TMDbTests/Networking/Mocks/MockURLProtocol.swift b/Tests/TMDbTests/Mocks/Networking/HTTPClient/MockURLProtocol.swift similarity index 100% rename from Tests/TMDbTests/Networking/Mocks/MockURLProtocol.swift rename to Tests/TMDbTests/Mocks/Networking/HTTPClient/MockURLProtocol.swift diff --git a/Tests/TMDbTests/Mocks/URL+Mocks.swift b/Tests/TMDbTests/Mocks/Networking/URL+Mocks.swift similarity index 100% rename from Tests/TMDbTests/Mocks/URL+Mocks.swift rename to Tests/TMDbTests/Mocks/Networking/URL+Mocks.swift diff --git a/Tests/TMDbTests/Models/TokenTests.swift b/Tests/TMDbTests/Models/TokenTests.swift new file mode 100644 index 00000000..40c09894 --- /dev/null +++ b/Tests/TMDbTests/Models/TokenTests.swift @@ -0,0 +1,39 @@ +// +// TokenTests.swift +// TMDb +// +// Copyright © 2024 Adam Young. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an AS IS BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@testable import TMDb +import XCTest + +final class TokenTests: XCTestCase { + + func testDecodeReturnsToken() throws { + let expectedResult = Token( + success: true, + requestToken: "10530f2246e244555d122016db7c65599c8d6f4d", + expiresAt: Date(timeIntervalSince1970: 1_705_956_596) + ) + + let result = try JSONDecoder.theMovieDatabaseAuth.decode(Token.self, fromResource: "request-token") + + XCTAssertEqual(result.success, expectedResult.success) + XCTAssertEqual(result.requestToken, expectedResult.requestToken) + XCTAssertEqual(result.expiresAt, expectedResult.expiresAt) + } + +} diff --git a/Tests/TMDbTests/Resources/json/request-token.json b/Tests/TMDbTests/Resources/json/request-token.json new file mode 100644 index 00000000..e8ffd9ea --- /dev/null +++ b/Tests/TMDbTests/Resources/json/request-token.json @@ -0,0 +1,5 @@ +{ + "success": true, + "expires_at": "2024-01-22 20:49:56 UTC", + "request_token": "10530f2246e244555d122016db7c65599c8d6f4d" +} diff --git a/Tests/TMDbTests/Services/Authentication/AuthenticationServiceTests.swift b/Tests/TMDbTests/Services/Authentication/AuthenticationServiceTests.swift new file mode 100644 index 00000000..d4d06084 --- /dev/null +++ b/Tests/TMDbTests/Services/Authentication/AuthenticationServiceTests.swift @@ -0,0 +1,122 @@ +// +// AuthenticationServiceTests.swift +// TMDb +// +// Copyright © 2024 Adam Young. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an AS IS BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@testable import TMDb +import XCTest + +final class AuthenticationServiceTests: XCTestCase { + + var service: AuthenticationService! + var apiClient: MockAPIClient! + var authenticateURLBuilder: AuthenticateURLMockBuilder! + + override func setUp() { + super.setUp() + apiClient = MockAPIClient() + authenticateURLBuilder = AuthenticateURLMockBuilder() + service = AuthenticationService(apiClient: apiClient, authenticateURLBuilder: authenticateURLBuilder) + } + + override func tearDown() { + service = nil + authenticateURLBuilder = nil + apiClient = nil + super.tearDown() + } + + func testGuestSessionReturnsGuestSession() async throws { + let expectedResult = GuestSession.mock() + + apiClient.result = .success(expectedResult) + + let result = try await service.guestSession() + + XCTAssertEqual(result, expectedResult) + XCTAssertEqual(apiClient.lastPath, AuthenticationEndpoint.createGuestSession.path) + } + + func testGuestSessionWhenErrorsThrowsError() async throws { + apiClient.result = .failure(.unknown) + + var error: Error? + do { + _ = try await service.guestSession() + } catch let err { + error = err + } + + let tmdbAPIError = try XCTUnwrap(error as? TMDbError) + + XCTAssertEqual(tmdbAPIError, .unknown) + } + + func testRequestTokenReturnsToken() async throws { + let expectedResult = Token.mock() + + apiClient.result = .success(expectedResult) + + let result = try await service.requestToken() + + XCTAssertEqual(result, expectedResult) + XCTAssertEqual(apiClient.lastPath, AuthenticationEndpoint.createRequestToken.path) + } + + func testRequestTokenWhenErrorsThrowsError() async throws { + apiClient.result = .failure(.unknown) + + var error: Error? + do { + _ = try await service.requestToken() + } catch let err { + error = err + } + + let tmdbAPIError = try XCTUnwrap(error as? TMDbError) + + XCTAssertEqual(tmdbAPIError, .unknown) + } + + func testAuthenticateURLReturnsURL() throws { + let expiresAt = Date(timeIntervalSince1970: 1_705_956_596) + let token = Token(success: true, requestToken: "abc123", expiresAt: expiresAt) + let expectedURL = try XCTUnwrap(URL(string: "https://some.domain.com/authenticate/abc123")) + authenticateURLBuilder.authenticateURLResult = expectedURL + + let url = service.authenticateURL(for: token) + + XCTAssertEqual(url, expectedURL) + XCTAssertEqual(authenticateURLBuilder.lastRequestToken, token.requestToken) + XCTAssertNil(authenticateURLBuilder.lastRedirectURL) + } + + func testAuthenticateURLWithRedirectURLReturnsURL() throws { + let expiresAt = Date(timeIntervalSince1970: 1_705_956_596) + let token = Token(success: true, requestToken: "abc123", expiresAt: expiresAt) + let redirectURL = try XCTUnwrap(URL(string: "https://some.domain.com/auth/callback")) + let expectedURL = try XCTUnwrap(URL(string: "https://some.domain.com/authenticate/abc123")) + authenticateURLBuilder.authenticateURLResult = expectedURL + + let url = service.authenticateURL(for: token, redirectURL: redirectURL) + + XCTAssertEqual(url, expectedURL) + XCTAssertEqual(authenticateURLBuilder.lastRequestToken, token.requestToken) + XCTAssertEqual(authenticateURLBuilder.lastRedirectURL, redirectURL) + } + +} diff --git a/Tests/TMDbTests/Authentication/Endpoints/AuthenticationEndpointTests.swift b/Tests/TMDbTests/Services/Authentication/Endpoints/AuthenticationEndpointTests.swift similarity index 79% rename from Tests/TMDbTests/Authentication/Endpoints/AuthenticationEndpointTests.swift rename to Tests/TMDbTests/Services/Authentication/Endpoints/AuthenticationEndpointTests.swift index 6585e43b..9e91e693 100644 --- a/Tests/TMDbTests/Authentication/Endpoints/AuthenticationEndpointTests.swift +++ b/Tests/TMDbTests/Services/Authentication/Endpoints/AuthenticationEndpointTests.swift @@ -30,4 +30,12 @@ final class AuthenticationEndpointTests: XCTestCase { XCTAssertEqual(url, expectedURL) } + func testCreateRequetTokenEndpointReturnsURL() throws { + let expectedURL = try XCTUnwrap(URL(string: "/authentication/token/new")) + + let url = AuthenticationEndpoint.createRequestToken.path + + XCTAssertEqual(url, expectedURL) + } + } diff --git a/Tests/TMDbTests/Certifications/CertificationServiceTests.swift b/Tests/TMDbTests/Services/Certifications/CertificationServiceTests.swift similarity index 100% rename from Tests/TMDbTests/Certifications/CertificationServiceTests.swift rename to Tests/TMDbTests/Services/Certifications/CertificationServiceTests.swift diff --git a/Tests/TMDbTests/Certifications/Endpoints/CertificationsEndpointTests.swift b/Tests/TMDbTests/Services/Certifications/Endpoints/CertificationsEndpointTests.swift similarity index 100% rename from Tests/TMDbTests/Certifications/Endpoints/CertificationsEndpointTests.swift rename to Tests/TMDbTests/Services/Certifications/Endpoints/CertificationsEndpointTests.swift diff --git a/Tests/TMDbTests/Company/CompanyServiceTests.swift b/Tests/TMDbTests/Services/Company/CompanyServiceTests.swift similarity index 100% rename from Tests/TMDbTests/Company/CompanyServiceTests.swift rename to Tests/TMDbTests/Services/Company/CompanyServiceTests.swift diff --git a/Tests/TMDbTests/Company/Endpoints/CompanyEndpointTests.swift b/Tests/TMDbTests/Services/Company/Endpoints/CompanyEndpointTests.swift similarity index 100% rename from Tests/TMDbTests/Company/Endpoints/CompanyEndpointTests.swift rename to Tests/TMDbTests/Services/Company/Endpoints/CompanyEndpointTests.swift diff --git a/Tests/TMDbTests/Configuration/ConfigurationServiceTests.swift b/Tests/TMDbTests/Services/Configuration/ConfigurationServiceTests.swift similarity index 100% rename from Tests/TMDbTests/Configuration/ConfigurationServiceTests.swift rename to Tests/TMDbTests/Services/Configuration/ConfigurationServiceTests.swift diff --git a/Tests/TMDbTests/Configuration/Endpoints/ConfigurationEndpointTests.swift b/Tests/TMDbTests/Services/Configuration/Endpoints/ConfigurationEndpointTests.swift similarity index 100% rename from Tests/TMDbTests/Configuration/Endpoints/ConfigurationEndpointTests.swift rename to Tests/TMDbTests/Services/Configuration/Endpoints/ConfigurationEndpointTests.swift diff --git a/Tests/TMDbTests/Discover/DiscoverServiceTests.swift b/Tests/TMDbTests/Services/Discover/DiscoverServiceTests.swift similarity index 100% rename from Tests/TMDbTests/Discover/DiscoverServiceTests.swift rename to Tests/TMDbTests/Services/Discover/DiscoverServiceTests.swift diff --git a/Tests/TMDbTests/Discover/Endpoints/DiscoverEndpointTests.swift b/Tests/TMDbTests/Services/Discover/Endpoints/DiscoverEndpointTests.swift similarity index 100% rename from Tests/TMDbTests/Discover/Endpoints/DiscoverEndpointTests.swift rename to Tests/TMDbTests/Services/Discover/Endpoints/DiscoverEndpointTests.swift diff --git a/Tests/TMDbTests/Discover/MovieSortTests.swift b/Tests/TMDbTests/Services/Discover/MovieSortTests.swift similarity index 100% rename from Tests/TMDbTests/Discover/MovieSortTests.swift rename to Tests/TMDbTests/Services/Discover/MovieSortTests.swift diff --git a/Tests/TMDbTests/Discover/TVSeriesSortTests.swift b/Tests/TMDbTests/Services/Discover/TVSeriesSortTests.swift similarity index 100% rename from Tests/TMDbTests/Discover/TVSeriesSortTests.swift rename to Tests/TMDbTests/Services/Discover/TVSeriesSortTests.swift diff --git a/Tests/TMDbTests/Genres/Endpoints/GenresEndpointTests.swift b/Tests/TMDbTests/Services/Genres/Endpoints/GenresEndpointTests.swift similarity index 100% rename from Tests/TMDbTests/Genres/Endpoints/GenresEndpointTests.swift rename to Tests/TMDbTests/Services/Genres/Endpoints/GenresEndpointTests.swift diff --git a/Tests/TMDbTests/Genres/GenreServiceTests.swift b/Tests/TMDbTests/Services/Genres/GenreServiceTests.swift similarity index 100% rename from Tests/TMDbTests/Genres/GenreServiceTests.swift rename to Tests/TMDbTests/Services/Genres/GenreServiceTests.swift diff --git a/Tests/TMDbTests/Movies/Endpoints/MoviesEndpointTests.swift b/Tests/TMDbTests/Services/Movies/Endpoints/MoviesEndpointTests.swift similarity index 100% rename from Tests/TMDbTests/Movies/Endpoints/MoviesEndpointTests.swift rename to Tests/TMDbTests/Services/Movies/Endpoints/MoviesEndpointTests.swift diff --git a/Tests/TMDbTests/Movies/MovieServiceTests.swift b/Tests/TMDbTests/Services/Movies/MovieServiceTests.swift similarity index 100% rename from Tests/TMDbTests/Movies/MovieServiceTests.swift rename to Tests/TMDbTests/Services/Movies/MovieServiceTests.swift diff --git a/Tests/TMDbTests/People/Endpoints/PeopleEndpointTests.swift b/Tests/TMDbTests/Services/People/Endpoints/PeopleEndpointTests.swift similarity index 100% rename from Tests/TMDbTests/People/Endpoints/PeopleEndpointTests.swift rename to Tests/TMDbTests/Services/People/Endpoints/PeopleEndpointTests.swift diff --git a/Tests/TMDbTests/People/PersonServiceTests.swift b/Tests/TMDbTests/Services/People/PersonServiceTests.swift similarity index 100% rename from Tests/TMDbTests/People/PersonServiceTests.swift rename to Tests/TMDbTests/Services/People/PersonServiceTests.swift diff --git a/Tests/TMDbTests/Search/Endpoints/SearchEndpointTests.swift b/Tests/TMDbTests/Services/Search/Endpoints/SearchEndpointTests.swift similarity index 100% rename from Tests/TMDbTests/Search/Endpoints/SearchEndpointTests.swift rename to Tests/TMDbTests/Services/Search/Endpoints/SearchEndpointTests.swift diff --git a/Tests/TMDbTests/Search/SearchServiceTests.swift b/Tests/TMDbTests/Services/Search/SearchServiceTests.swift similarity index 100% rename from Tests/TMDbTests/Search/SearchServiceTests.swift rename to Tests/TMDbTests/Services/Search/SearchServiceTests.swift diff --git a/Tests/TMDbTests/TVEpisodes/ENdpoints/TVEpisodesEndpointTests.swift b/Tests/TMDbTests/Services/TVEpisodes/Endpoints/TVEpisodesEndpointTests.swift similarity index 100% rename from Tests/TMDbTests/TVEpisodes/ENdpoints/TVEpisodesEndpointTests.swift rename to Tests/TMDbTests/Services/TVEpisodes/Endpoints/TVEpisodesEndpointTests.swift diff --git a/Tests/TMDbTests/TVEpisodes/TVEpisodeServiceTests.swift b/Tests/TMDbTests/Services/TVEpisodes/TVEpisodeServiceTests.swift similarity index 100% rename from Tests/TMDbTests/TVEpisodes/TVEpisodeServiceTests.swift rename to Tests/TMDbTests/Services/TVEpisodes/TVEpisodeServiceTests.swift diff --git a/Tests/TMDbTests/TVSeasons/Endpoints/TVSeasonsEndpointTests.swift b/Tests/TMDbTests/Services/TVSeasons/Endpoints/TVSeasonsEndpointTests.swift similarity index 100% rename from Tests/TMDbTests/TVSeasons/Endpoints/TVSeasonsEndpointTests.swift rename to Tests/TMDbTests/Services/TVSeasons/Endpoints/TVSeasonsEndpointTests.swift diff --git a/Tests/TMDbTests/TVSeasons/TVSeasonServiceTests.swift b/Tests/TMDbTests/Services/TVSeasons/TVSeasonServiceTests.swift similarity index 100% rename from Tests/TMDbTests/TVSeasons/TVSeasonServiceTests.swift rename to Tests/TMDbTests/Services/TVSeasons/TVSeasonServiceTests.swift diff --git a/Tests/TMDbTests/TVSeries/Endpoints/TVSeriesEndpointTests.swift b/Tests/TMDbTests/Services/TVSeries/Endpoints/TVSeriesEndpointTests.swift similarity index 100% rename from Tests/TMDbTests/TVSeries/Endpoints/TVSeriesEndpointTests.swift rename to Tests/TMDbTests/Services/TVSeries/Endpoints/TVSeriesEndpointTests.swift diff --git a/Tests/TMDbTests/TVSeries/TVSeriesServiceTests.swift b/Tests/TMDbTests/Services/TVSeries/TVSeriesServiceTests.swift similarity index 100% rename from Tests/TMDbTests/TVSeries/TVSeriesServiceTests.swift rename to Tests/TMDbTests/Services/TVSeries/TVSeriesServiceTests.swift diff --git a/Tests/TMDbTests/Trending/Endpoints/TrendingEndpointTests.swift b/Tests/TMDbTests/Services/Trending/Endpoints/TrendingEndpointTests.swift similarity index 100% rename from Tests/TMDbTests/Trending/Endpoints/TrendingEndpointTests.swift rename to Tests/TMDbTests/Services/Trending/Endpoints/TrendingEndpointTests.swift diff --git a/Tests/TMDbTests/Trending/TrendingServiceTests.swift b/Tests/TMDbTests/Services/Trending/TrendingServiceTests.swift similarity index 100% rename from Tests/TMDbTests/Trending/TrendingServiceTests.swift rename to Tests/TMDbTests/Services/Trending/TrendingServiceTests.swift diff --git a/Tests/TMDbTests/Trending/TrendingTimeWindowFilterTypeTests.swift b/Tests/TMDbTests/Services/Trending/TrendingTimeWindowFilterTypeTests.swift similarity index 100% rename from Tests/TMDbTests/Trending/TrendingTimeWindowFilterTypeTests.swift rename to Tests/TMDbTests/Services/Trending/TrendingTimeWindowFilterTypeTests.swift diff --git a/Tests/TMDbTests/WatchProviders/Endpoints/WatchProviderEndpointTests.swift b/Tests/TMDbTests/Services/WatchProviders/Endpoints/WatchProviderEndpointTests.swift similarity index 100% rename from Tests/TMDbTests/WatchProviders/Endpoints/WatchProviderEndpointTests.swift rename to Tests/TMDbTests/Services/WatchProviders/Endpoints/WatchProviderEndpointTests.swift diff --git a/Tests/TMDbTests/WatchProviders/WatchProviderServiceTests.swift b/Tests/TMDbTests/Services/WatchProviders/WatchProviderServiceTests.swift similarity index 100% rename from Tests/TMDbTests/WatchProviders/WatchProviderServiceTests.swift rename to Tests/TMDbTests/Services/WatchProviders/WatchProviderServiceTests.swift diff --git a/Tests/TMDbTests/Utilities/Data+LoadFromFile.swift b/Tests/TMDbTests/TestUtils/Data+LoadFromFile.swift similarity index 100% rename from Tests/TMDbTests/Utilities/Data+LoadFromFile.swift rename to Tests/TMDbTests/TestUtils/Data+LoadFromFile.swift diff --git a/Tests/TMDbTests/Utilities/Date+RandomDate.swift b/Tests/TMDbTests/TestUtils/Date+RandomDate.swift similarity index 100% rename from Tests/TMDbTests/Utilities/Date+RandomDate.swift rename to Tests/TMDbTests/TestUtils/Date+RandomDate.swift diff --git a/Tests/TMDbTests/Utilities/Int+RandomID.swift b/Tests/TMDbTests/TestUtils/Int+RandomID.swift similarity index 100% rename from Tests/TMDbTests/Utilities/Int+RandomID.swift rename to Tests/TMDbTests/TestUtils/Int+RandomID.swift diff --git a/Tests/TMDbTests/Utilities/JSONDecoder+DecodeFromFile.swift b/Tests/TMDbTests/TestUtils/JSONDecoder+DecodeFromFile.swift similarity index 100% rename from Tests/TMDbTests/Utilities/JSONDecoder+DecodeFromFile.swift rename to Tests/TMDbTests/TestUtils/JSONDecoder+DecodeFromFile.swift diff --git a/Tests/TMDbTests/Utilities/JSONEncoder+TMDb.swift b/Tests/TMDbTests/TestUtils/JSONEncoder+TMDb.swift similarity index 100% rename from Tests/TMDbTests/Utilities/JSONEncoder+TMDb.swift rename to Tests/TMDbTests/TestUtils/JSONEncoder+TMDb.swift diff --git a/Tests/TMDbTests/Utilities/String+RandomID.swift b/Tests/TMDbTests/TestUtils/String+RandomID.swift similarity index 100% rename from Tests/TMDbTests/Utilities/String+RandomID.swift rename to Tests/TMDbTests/TestUtils/String+RandomID.swift