From 71de1ed9bed4e7569e13a8e7ef65b1a06a31c7bc Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 29 Jan 2021 19:38:11 +0800 Subject: [PATCH] feat: add OAuth API endpoint unit test --- Mastodon.xcodeproj/project.pbxproj | 53 ++++++++++ .../xcshareddata/swiftpm/Package.resolved | 9 ++ Mastodon/Extension/OSLog.swift | 20 ++++ .../AuthenticationViewController.swift | 10 ++ ...PinBasedAuthenticationViewController.swift | 25 +++++ ...todonPinBasedAuthenticationViewModel.swift | 12 +++ ...ationViewModelNavigationDelegateShim.swift | 34 +++++++ MastodonSDK.xctestplan | 25 ++++- .../MastodonSDK/API/Mastodon+API+App.swift | 54 +++++++++- .../MastodonSDK/API/Mastodon+API+OAuth.swift | 99 +++++++++++++++++++ .../API/Mastodon+API+Timeline.swift | 30 ++---- .../MastodonSDK/API/Mastodon+API.swift | 22 +++-- .../API/MastodonSDK+API+AppTests.swift | 83 ++++++++++++++++ .../API/MastodonSDK+API+OAuthTests.swift | 29 ++++++ .../MastodonSDKTests/MastodonSDKTests.swift | 55 ++--------- 15 files changed, 482 insertions(+), 78 deletions(-) create mode 100644 Mastodon/Extension/OSLog.swift create mode 100644 Mastodon/Scene/Authentication/AuthenticationViewController.swift create mode 100644 Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewController.swift create mode 100644 Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModel.swift create mode 100644 Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift create mode 100644 MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+AppTests.swift create mode 100644 MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+OAuthTests.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 4cef307c8..8841f7ee3 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -11,6 +11,12 @@ 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; 7A9135D4559750AF07CA9BE4 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */; }; + DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */; }; + DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; }; + DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; }; + DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; }; + DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; }; + DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; }; DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; }; DB3D102425BAA7B400EAA174 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3D102225BAA7B400EAA174 /* Assets.swift */; }; @@ -108,6 +114,11 @@ A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = ""; }; CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = ""; }; + DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewController.swift; sourceTree = ""; }; + DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift; sourceTree = ""; }; + DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = ""; }; + DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = ""; }; DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; DB3D102225BAA7B400EAA174 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; @@ -160,6 +171,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */, DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, 7A9135D4559750AF07CA9BE4 /* Pods_Mastodon.framework in Frameworks */, @@ -224,6 +236,25 @@ name = Frameworks; sourceTree = ""; }; + DB01409B25C40BB600F9F3CF /* Authentication */ = { + isa = PBXGroup; + children = ( + DB0140A625C40C0900F9F3CF /* PinBased */, + DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */, + ); + path = Authentication; + sourceTree = ""; + }; + DB0140A625C40C0900F9F3CF /* PinBased */ = { + isa = PBXGroup; + children = ( + DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */, + DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */, + DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */, + ); + path = PinBased; + sourceTree = ""; + }; DB3D0FF725BAA68500EAA174 /* Supporting Files */ = { isa = PBXGroup; children = ( @@ -400,6 +431,7 @@ children = ( DB8AF54E25C13703002E6C99 /* MainTab */, DB8AF55625C137A8002E6C99 /* HomeViewController.swift */, + DB01409B25C40BB600F9F3CF /* Authentication */, ); path = Scene; sourceTree = ""; @@ -407,6 +439,7 @@ DB8AF56225C138BC002E6C99 /* Extension */ = { isa = PBXGroup; children = ( + DB0140CE25C42AEE00F9F3CF /* OSLog.swift */, DB8AF55C25C138B7002E6C99 /* UIViewController.swift */, ); path = Extension; @@ -447,6 +480,7 @@ packageProductDependencies = ( DB3D0FF225BAA61700EAA174 /* AlamofireImage */, 5D526FE125BE9AC400460CB9 /* MastodonSDK */, + DB0140BC25C40D7500F9F3CF /* CommonOSLog */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -569,6 +603,7 @@ mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */, + DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -753,15 +788,20 @@ DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */, DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, + DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */, DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */, DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */, + DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, + DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */, DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */, DB8AF55725C137A8002E6C99 /* HomeViewController.swift in Sources */, DB3D102525BAA7B400EAA174 /* Strings.swift in Sources */, + DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */, DB3D102425BAA7B400EAA174 /* Assets.swift in Sources */, DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */, + DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1263,6 +1303,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/MainasuK/CommonOSLog"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.1.1; + }; + }; DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireImage.git"; @@ -1278,6 +1326,11 @@ isa = XCSwiftPackageProductDependency; productName = MastodonSDK; }; + DB0140BC25C40D7500F9F3CF /* CommonOSLog */ = { + isa = XCSwiftPackageProductDependency; + package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; + productName = CommonOSLog; + }; DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = { isa = XCSwiftPackageProductDependency; package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 17197be49..296f9dd2a 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -19,6 +19,15 @@ "version": "4.1.0" } }, + { + "package": "CommonOSLog", + "repositoryURL": "https://github.com/MainasuK/CommonOSLog", + "state": { + "branch": null, + "revision": "c121624a30698e9886efe38aebb36ff51c01b6c2", + "version": "0.1.1" + } + }, { "package": "swift-nio", "repositoryURL": "https://github.com/apple/swift-nio.git", diff --git a/Mastodon/Extension/OSLog.swift b/Mastodon/Extension/OSLog.swift new file mode 100644 index 000000000..0121200d9 --- /dev/null +++ b/Mastodon/Extension/OSLog.swift @@ -0,0 +1,20 @@ +// +// OSLog.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021/1/29 +// + +import os +import Foundation +import CommonOSLog + +extension OSLog { + static let api: OSLog = { + #if DEBUG + return OSLog(subsystem: OSLog.subsystem + ".api", category: "api") + #else + return OSLog.disabled + #endif + }() +} diff --git a/Mastodon/Scene/Authentication/AuthenticationViewController.swift b/Mastodon/Scene/Authentication/AuthenticationViewController.swift new file mode 100644 index 000000000..12e1d14d5 --- /dev/null +++ b/Mastodon/Scene/Authentication/AuthenticationViewController.swift @@ -0,0 +1,10 @@ +// +// AuthenticationViewController.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021/1/29. +// + +import UIKit + + diff --git a/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewController.swift b/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewController.swift new file mode 100644 index 000000000..6f9ce506f --- /dev/null +++ b/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewController.swift @@ -0,0 +1,25 @@ +// +// MastodonPinBasedAuthenticationViewController.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021/1/29. +// + +import os.log +import Foundation +import WebKit + +final class MastodonPinBasedAuthenticationViewController: NSObject { + + + weak var viewModel: MastodonPinBasedAuthenticationViewModel? + + init(viewModel: MastodonPinBasedAuthenticationViewModel) { + self.viewModel = viewModel + } + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} diff --git a/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModel.swift b/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModel.swift new file mode 100644 index 000000000..4ae5e14a4 --- /dev/null +++ b/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModel.swift @@ -0,0 +1,12 @@ +// +// MastodonPinBasedAuthenticationViewModel.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021/1/29. +// + +import Foundation + +final class MastodonPinBasedAuthenticationViewModel { + +} diff --git a/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift b/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift new file mode 100644 index 000000000..3f7092b1f --- /dev/null +++ b/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift @@ -0,0 +1,34 @@ +// +// MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021/1/29. +// + +import os.log +import Foundation +import WebKit + +final class MastodonPinBasedAuthenticationViewModelNavigationDelegateShim: NSObject { + + weak var viewModel: MastodonPinBasedAuthenticationViewModel? + + init(viewModel: MastodonPinBasedAuthenticationViewModel) { + self.viewModel = viewModel + } + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } +} + + +// MARK: - WKNavigationDelegate +extension MastodonPinBasedAuthenticationViewModelNavigationDelegateShim: WKNavigationDelegate { + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + // TODO: + } + +} + diff --git a/MastodonSDK.xctestplan b/MastodonSDK.xctestplan index 14cf031db..28e9be637 100644 --- a/MastodonSDK.xctestplan +++ b/MastodonSDK.xctestplan @@ -2,9 +2,26 @@ "configurations" : [ { "id" : "5119353D-C795-4264-89FD-8376D9B144F8", - "name" : "Configuration 1", + "name" : "mstdn.jp", "options" : { - + "environmentVariableEntries" : [ + { + "key" : "domain", + "value" : "mstdn.jp" + } + ] + } + }, + { + "id" : "C5184AF3-B83B-4A7E-949C-6B1AA3ABE7D1", + "name" : "pawoo.net", + "options" : { + "environmentVariableEntries" : [ + { + "key" : "domain", + "value" : "pawoo.net" + } + ] } } ], @@ -13,6 +30,10 @@ }, "testTargets" : [ { + "skippedTests" : [ + "MastodonSDKTests\/testCreateAnAnpplication()", + "MastodonSDKTests\/testVerifyAppCredentials()" + ], "target" : { "containerPath" : "container:MastodonSDK", "identifier" : "MastodonSDKTests", diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift index 987164614..54105790f 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift @@ -14,12 +14,31 @@ extension Mastodon.API.App { return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("apps") } + static func verifyCredentialsEndpointURL(domain: String) -> URL { + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("apps/verify_credentials") + } + + /// Create an application + /// + /// Using this endpoint to obtain `client_id` and `client_secret` for later OAuth token exchange + /// + /// - Since: 0.0.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/1/29 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/apps/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - query: `CreateQuery` + /// - Returns: `AnyPublisher` contains `Application` nested in the response public static func create( session: URLSession, domain: String, query: CreateQuery ) -> AnyPublisher, Error> { - let request = Mastodon.API.request( + let request = Mastodon.API.post( url: appEndpointURL(domain: domain), query: query, authorization: nil @@ -31,6 +50,39 @@ extension Mastodon.API.App { } .eraseToAnyPublisher() } + + /// Verify application token + /// + /// Using this endpoint to verify App token + /// + /// - Since: 2.0.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/1/29 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/apps/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - authorization: App token + /// - Returns: `AnyPublisher` contains `Application` nested in the response + public static func verifyCredentials( + session: URLSession, + domain: String, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let request = Mastodon.API.get( + url: verifyCredentialsEndpointURL(domain: domain), + query: nil, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.Application.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift index 88461f995..c25526ff6 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift @@ -6,6 +6,7 @@ // import Foundation +import Combine extension Mastodon.API.OAuth { @@ -16,3 +17,101 @@ extension Mastodon.API.OAuth { } } + +extension Mastodon.API.OAuth { + + static func authorizeEndpointURL(domain: String) -> URL { + return Mastodon.API.oauthEndpointURL(domain: domain).appendingPathComponent("authorize") + } + + /// Construct user authorize endpoint URL + /// + /// This method construct a URL for user authorize + /// + /// - Since: 0.1.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/1/29 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/apps/oauth/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - query: `AuthorizeQuery` + static func authorizeURL( + domain: String, + query: AuthorizeQuery + ) -> URL { + let request = Mastodon.API.get( + url: authorizeEndpointURL(domain: domain), + query: query, + authorization: nil + ) + let url = request.url! + return url + } + +// static func authorize( +// session: URLSession, +// domain: String, +// query: AuthorizeQuery +// ) -> AnyPublisher, Error> { +// let request = Mastodon.API.post( +// url: authorizeEndpointURL(domain: domain), +// query: query, +// authorization: nil +// ) +// return session.dataTaskPublisher(for: request) +// .tryMap { data, response in +// let value = try Mastodon.API.decode(type: Mastodon.Entity.Token.self, from: data, response: response) +// return Mastodon.Response.Content(value: value, response: response) +// } +// .eraseToAnyPublisher() +// } + +} + +extension Mastodon.API.OAuth { + public struct AuthorizeQuery: GetQuery { + + public let forceLogin: String? + public let responseType: String + public let clientID: String + public let redirectURI: String + public let scope: String? + + public init( + forceLogin: String? = nil, + responseType: String = "code", + clientID: String, + redirectURI: String = "urn:ietf:wg:oauth:2.0:oob", + scope: String? = "read write follow push" + ) { + self.forceLogin = forceLogin + self.responseType = responseType + self.clientID = clientID + self.redirectURI = redirectURI + self.scope = scope + } + + enum CodingKeys: String, CodingKey { + case forceLogin = "force_login" + case responseType = "response_type" + case clientID + case redirectURI = "redirect_uri" + case scope + } + + var queryItems: [URLQueryItem]? { + var items: [URLQueryItem] = [] + forceLogin.flatMap { items.append(URLQueryItem(name: "force_login", value: $0)) } + items.append(URLQueryItem(name: "response_type", value: responseType)) + items.append(URLQueryItem(name: "clientID", value: clientID)) + items.append(URLQueryItem(name: "redirect_uri", value: redirectURI)) + scope.flatMap { items.append(URLQueryItem(name: "scope", value: $0)) } + guard !items.isEmpty else { return nil } + return items + } + + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Timeline.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Timeline.swift index 13d0dc141..95f912b41 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Timeline.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Timeline.swift @@ -19,7 +19,7 @@ extension Mastodon.API.Timeline { domain: String, query: PublicTimelineQuery ) -> AnyPublisher, Error> { - let request = Mastodon.API.request( + let request = Mastodon.API.get( url: publicTimelineEndpointURL(domain: domain), query: query, authorization: nil @@ -65,27 +65,13 @@ extension Mastodon.API.Timeline { var queryItems: [URLQueryItem]? { var items: [URLQueryItem] = [] - local.flatMap { - items.append(URLQueryItem(name: "local", value: $0.queryItemValue)) - } - remote.flatMap { - items.append(URLQueryItem(name: "remote", value: $0.queryItemValue)) - } - onlyMedia.flatMap { - items.append(URLQueryItem(name: "only_media", value: $0.queryItemValue)) - } - maxID.flatMap { - items.append(URLQueryItem(name: "max_id", value: $0)) - } - sinceID.flatMap { - items.append(URLQueryItem(name: "since_id", value: $0)) - } - minID.flatMap { - items.append(URLQueryItem(name: "min_id", value: $0)) - } - limit.flatMap { - items.append(URLQueryItem(name: "limit", value: String($0))) - } + local.flatMap { items.append(URLQueryItem(name: "local", value: $0.queryItemValue)) } + remote.flatMap { items.append(URLQueryItem(name: "remote", value: $0.queryItemValue)) } + onlyMedia.flatMap { items.append(URLQueryItem(name: "only_media", value: $0.queryItemValue)) } + maxID.flatMap { items.append(URLQueryItem(name: "max_id", value: $0)) } + sinceID.flatMap { items.append(URLQueryItem(name: "since_id", value: $0)) } + minID.flatMap { items.append(URLQueryItem(name: "min_id", value: $0)) } + limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) } guard !items.isEmpty else { return nil } return items } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 9c3fee054..fcab0eb3e 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -53,9 +53,15 @@ extension Mastodon.API { return decoder }() + static func oauthEndpointURL(domain: String) -> URL { + return URL(string: "https://" + domain + "/oauth/")! + } static func endpointURL(domain: String) -> URL { return URL(string: "https://" + domain + "/api/v1/")! } + static func endpointV2URL(domain: String) -> URL { + return URL(string: "https://" + domain + "/api/v2/")! + } } @@ -67,13 +73,15 @@ extension Mastodon.API { extension Mastodon.API { - static func request( + static func get( url: URL, - query: GetQuery, + query: GetQuery?, authorization: OAuth.Authorization? ) -> URLRequest { var components = URLComponents(string: url.absoluteString)! - components.queryItems = query.queryItems + if let query = query { + components.queryItems = query.queryItems + } let requestURL = components.url! var request = URLRequest( @@ -91,9 +99,9 @@ extension Mastodon.API { return request } - static func request( + static func post( url: URL, - query: PostQuery, + query: PostQuery?, authorization: OAuth.Authorization? ) -> URLRequest { let components = URLComponents(string: url.absoluteString)! @@ -104,7 +112,9 @@ extension Mastodon.API { timeoutInterval: Mastodon.API.timeoutInterval ) request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") - request.httpBody = query.body + if let query = query { + request.httpBody = query.body + } if let authorization = authorization { request.setValue( "Bearer \(authorization.accessToken)", diff --git a/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+AppTests.swift b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+AppTests.swift new file mode 100644 index 000000000..f74fe61dc --- /dev/null +++ b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+AppTests.swift @@ -0,0 +1,83 @@ +// +// MastodonSDK+API+AppTests.swift +// +// +// Created by MainasuK Cirno on 2021/1/29. +// + +import os.log +import XCTest +import Combine +@testable import MastodonSDK + +extension MastodonSDKTests { + + func testCreateAnAnpplication() throws { + try _testCreateAnAnpplication(domain: domain) + } + + func _testCreateAnAnpplication(domain: String) throws { + let theExpectation = expectation(description: "Create An Application") + + let query = Mastodon.API.App.CreateQuery( + clientName: "XCTest", + website: nil + ) + Mastodon.API.App.create(session: session, domain: domain, query: query) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .failure(let error): + XCTFail(error.localizedDescription) + case .finished: + break + } + } receiveValue: { response in + XCTAssertEqual(response.value.name, "XCTest") + XCTAssertEqual(response.value.website, nil) + XCTAssertEqual(response.value.redirectURI, "urn:ietf:wg:oauth:2.0:oob") + os_log("%{public}s[%{public}ld], %{public}s: (%s) clientID %s", ((#file as NSString).lastPathComponent), #line, #function, domain, response.value.clientID ?? "nil") + os_log("%{public}s[%{public}ld], %{public}s: (%s) clientSecret %s", ((#file as NSString).lastPathComponent), #line, #function, domain, response.value.clientSecret ?? "nil") + theExpectation.fulfill() + } + .store(in: &disposeBag) + + wait(for: [theExpectation], timeout: 5.0) + } + +} + +extension MastodonSDKTests { + + func testVerifyAppCredentials() throws { + try _testVerifyAppCredentials(domain: domain, accessToken: "") + } + + func _testVerifyAppCredentials(domain: String, accessToken: String) throws { + let theExpectation = expectation(description: "Verify App Credentials") + + let authorization = Mastodon.API.OAuth.Authorization(accessToken: accessToken) + Mastodon.API.App.verifyCredentials( + session: session, + domain: domain, + authorization: authorization + ) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .failure(let error): + XCTFail(error.localizedDescription) + case .finished: + break + } + } receiveValue: { response in + XCTAssertEqual(response.value.name, "XCTest") + XCTAssertEqual(response.value.website, nil) + theExpectation.fulfill() + } + .store(in: &disposeBag) + + wait(for: [theExpectation], timeout: 5.0) + } + +} diff --git a/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+OAuthTests.swift b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+OAuthTests.swift new file mode 100644 index 000000000..b027578f1 --- /dev/null +++ b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+OAuthTests.swift @@ -0,0 +1,29 @@ +// +// MastodonSDK+API+OAuthTests.swift +// +// +// Created by MainasuK Cirno on 2021/1/29. +// + +import os.log +import XCTest +import Combine +@testable import MastodonSDK + +extension MastodonSDKTests { + + func testOAuthAuthorize() throws { + try _testOAuthAuthorize(domain: domain) + } + + func _testOAuthAuthorize(domain: String) throws { + let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: "StubClientID") + let authorizeURL = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query) + os_log("%{public}s[%{public}ld], %{public}s: (%s) authorizeURL %s", ((#file as NSString).lastPathComponent), #line, #function, domain, authorizeURL.absoluteString) + XCTAssertEqual( + authorizeURL.absoluteString, + "https://\(domain)/oauth/authorize?response_type=code&clientID=StubClientID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=read%20write%20follow%20push" + ) + } + +} diff --git a/MastodonSDK/Tests/MastodonSDKTests/MastodonSDKTests.swift b/MastodonSDK/Tests/MastodonSDKTests/MastodonSDKTests.swift index 15d2cee00..8996d1aa8 100644 --- a/MastodonSDK/Tests/MastodonSDKTests/MastodonSDKTests.swift +++ b/MastodonSDK/Tests/MastodonSDKTests/MastodonSDKTests.swift @@ -5,59 +5,20 @@ import Combine final class MastodonSDKTests: XCTestCase { var disposeBag = Set() - - let mstdnDomain = "mstdn.jp" - let pawooDomain = "pawoo.net" + let session = URLSession(configuration: .ephemeral) + var domain: String { MastodonSDKTests.environmentVariable(key: "domain") } + + static func environmentVariable(key: String) -> String { + return ProcessInfo.processInfo.environment[key]! + } } extension MastodonSDKTests { - func testCreateAnAnpplication_mstdn() throws { - try _testCreateAnAnpplication(domain: pawooDomain) - } - - func testCreateAnAnpplication_pawoo() throws { - try _testCreateAnAnpplication(domain: pawooDomain) - } - - func _testCreateAnAnpplication(domain: String) throws { - let theExpectation = expectation(description: "Create An Application") - - let query = Mastodon.API.App.CreateQuery( - clientName: "XCTest", - website: nil - ) - Mastodon.API.App.create(session: session, domain: domain, query: query) - .receive(on: DispatchQueue.main) - .sink { completion in - switch completion { - case .failure(let error): - XCTFail(error.localizedDescription) - case .finished: - break - } - } receiveValue: { response in - XCTAssertEqual(response.value.name, "XCTest") - XCTAssertEqual(response.value.website, nil) - XCTAssertEqual(response.value.redirectURI, "urn:ietf:wg:oauth:2.0:oob") - theExpectation.fulfill() - } - .store(in: &disposeBag) - - wait(for: [theExpectation], timeout: 10.0) - } -} - -extension MastodonSDKTests { - - func testPublicTimeline_mstdn() throws { - try _testPublicTimeline(domain: mstdnDomain) - } - - func testPublicTimeline_pawoo() throws { - try _testPublicTimeline(domain: pawooDomain) + func testPublicTimeline() throws { + try _testPublicTimeline(domain: domain) } private func _testPublicTimeline(domain: String) throws {