From 1ad3f1200f65c830f7d99fd9f9d479d2770e97f3 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:06:23 -0700 Subject: [PATCH 1/5] Use Swift 6 for the package --- Dockerfile.swift | 2 +- Package.swift | 9 +++++++-- .../Sources/wordpress-api/SafeRequestExecutor.swift | 9 ++------- native/swift/Sources/wordpress-api/WordPressAPI.swift | 7 ++----- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Dockerfile.swift b/Dockerfile.swift index 5259bdc90..1679fa679 100644 --- a/Dockerfile.swift +++ b/Dockerfile.swift @@ -1,4 +1,4 @@ -FROM public.ecr.aws/docker/library/swift:5.10 +FROM public.ecr.aws/docker/library/swift:6.0 RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ && apt-get update \ diff --git a/Package.swift b/Package.swift index 15407858c..fe58a6f88 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.10 +// swift-tools-version: 6.0 import Foundation import PackageDescription @@ -48,6 +48,9 @@ var package = Package( path: "native/swift/Sources/wordpress-api-wrapper", exclude: [ "README.md" + ], + swiftSettings: [ + .swiftLanguageMode(.v5) ] ), libwordpressFFI, @@ -99,9 +102,11 @@ enum WordPressRSVersion { } // Add SwiftLint to the package so that we can see linting issues directly from Xcode. +@MainActor func enableSwiftLint() throws { #if os(macOS) - let version = try String(contentsOf: URL(string:"./.swiftlint.yml", relativeTo: URL(filePath: #filePath))!) + let filePath = URL(string:"./.swiftlint.yml", relativeTo: URL(filePath: #filePath))! + let version = try String(contentsOf: filePath, encoding: .utf8) .split(separator: "\n") .first(where: { $0.starts(with: "swiftlint_version") })? .split(separator: ":") diff --git a/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift b/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift index 035b85aaa..3449f7390 100644 --- a/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift +++ b/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift @@ -8,9 +8,7 @@ import FoundationNetworking #endif public protocol SafeRequestExecutor: RequestExecutor { - func execute(_ request: WpNetworkRequest) async -> Result - } extension SafeRequestExecutor { @@ -22,9 +20,7 @@ extension SafeRequestExecutor { } -#if hasFeature(RetroactiveAttribute) -extension URLSession: @retroactive RequestExecutor {} -#endif +extension URLSession: RequestExecutor {} extension URLSession: SafeRequestExecutor { @@ -36,9 +32,8 @@ extension URLSession: SafeRequestExecutor { return .failure(.RequestExecutionFailed(statusCode: nil, reason: error.localizedDescription)) } - // swiftlint:disable force_cast + // swiftlint:disable:next force_cast let urlResponse = response as! HTTPURLResponse - // swiftlint:enable force_cast let headerMap: WpNetworkHeaderMap diff --git a/native/swift/Sources/wordpress-api/WordPressAPI.swift b/native/swift/Sources/wordpress-api/WordPressAPI.swift index b3935ed61..b45a8d50f 100644 --- a/native/swift/Sources/wordpress-api/WordPressAPI.swift +++ b/native/swift/Sources/wordpress-api/WordPressAPI.swift @@ -68,11 +68,8 @@ public struct WordPressAPI { } package func perform(request: WpNetworkRequest) async throws -> WpNetworkResponse { - try await withCheckedThrowingContinuation { continuation in - self.perform(request: request) { result in - continuation.resume(with: result) - } - } + let (data, response) = try await self.urlSession.data(for: request.asURLRequest()) + return try WpNetworkResponse.from(data: data, response: response) } package func perform( From d8ae290d5e1d9d6ec09f736f8025b5eaf2e932cf Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 13 Nov 2024 19:53:57 -0700 Subject: [PATCH 2/5] Fix Swift 6 Test Issues --- Package.resolved | 3 +- .../Tests/wordpress-api/Endpoints/Users.swift | 5 +- .../Tests/wordpress-api/HTTPErrorTests.swift | 3 +- .../Tests/wordpress-api/LoginTests.swift | 152 +++++++++--------- .../wordpress-api/Support/Extensions.swift | 14 ++ .../wordpress-api/Support/HTTPStubs.swift | 35 ++-- .../wordpress-api/WordPressAPITests.swift | 8 +- 7 files changed, 120 insertions(+), 100 deletions(-) create mode 100644 native/swift/Tests/wordpress-api/Support/Extensions.swift diff --git a/Package.resolved b/Package.resolved index 62c52f596..11857128b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "0385aaff7e0d6e30186b9f42ad615ad3dbcb283b4464c0202bebb483e0b4058f", "pins" : [ { "identity" : "collectionconcurrencykit", @@ -82,5 +83,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/native/swift/Tests/wordpress-api/Endpoints/Users.swift b/native/swift/Tests/wordpress-api/Endpoints/Users.swift index b8484063d..b9718e288 100644 --- a/native/swift/Tests/wordpress-api/Endpoints/Users.swift +++ b/native/swift/Tests/wordpress-api/Endpoints/Users.swift @@ -34,8 +34,9 @@ final class UsersTest: XCTestCase { } } """ - let stubs = HTTPStubs() - try stubs.stub(path: "/wp-json/wp/v2/users/1", with: .json(response)) + let stubs = HTTPStubs(stubs: [ + try HTTPStubs.stub(path: "/wp-json/wp/v2/users/1", with: .json(response)) + ]) let api = try WordPressAPI( urlSession: .shared, diff --git a/native/swift/Tests/wordpress-api/HTTPErrorTests.swift b/native/swift/Tests/wordpress-api/HTTPErrorTests.swift index 08b2c9271..db8d0a09d 100644 --- a/native/swift/Tests/wordpress-api/HTTPErrorTests.swift +++ b/native/swift/Tests/wordpress-api/HTTPErrorTests.swift @@ -12,8 +12,7 @@ class HTTPErrorTests: XCTestCase { #if !os(Linux) // Skip on Linux, because `XCTExpectFailure` is unavailable on Linux func testTimeout() async throws { - let stubs = HTTPStubs() - stubs.missingStub = .failure(URLError(.timedOut)) + let stubs = HTTPStubs(stubs: [], missingStub: .failure(URLError(.timedOut))) let api = try WordPressAPI( urlSession: .shared, diff --git a/native/swift/Tests/wordpress-api/LoginTests.swift b/native/swift/Tests/wordpress-api/LoginTests.swift index c511e2cf8..01c7e0156 100644 --- a/native/swift/Tests/wordpress-api/LoginTests.swift +++ b/native/swift/Tests/wordpress-api/LoginTests.swift @@ -12,15 +12,8 @@ class LoginTests: XCTestCase { // swiftlint:disable:next force_try let appId = { try! WpUuid.parse(input: "caa8b54a-eb5e-4134-8ae2-a3946a428ec7") }() - var stubs: HTTPStubs! - - override func setUp() { - super.setUp() - stubs = HTTPStubs() - stubs.missingStub = .failure(URLError(.timedOut)) - } - func testInvalidUrl() async { + let stubs = HTTPStubs(stubs: [], missingStub: .failure(URLError(.timedOut))) let client = WordPressLoginClient(requestExecutor: stubs) do { let result = await client.login( @@ -39,10 +32,13 @@ class LoginTests: XCTestCase { } func testNotWordPressSite() async throws { - try stubs.stub( - host: "example.com", - with: WpNetworkResponse(body: Data(), statusCode: 200, headerMap: .fromMap(hashMap: [:])) - ) + let stubs = HTTPStubs(stubs: [ + HTTPStubs.stub( + host: "example.com", + with: WpNetworkResponse(body: Data(), statusCode: 200, headerMap: .empty) + ) + ]) + let client = WordPressLoginClient(requestExecutor: stubs) do { let result = await client.login( @@ -71,22 +67,24 @@ class LoginTests: XCTestCase { } func testWpJsonError() async throws { - try stubs.stub( - url: "https://example.com/", - with: WpNetworkResponse( - body: Data(), - statusCode: 200, - headerMap: .fromMap(hashMap: ["Link": #"; rel="https://api.w.org/""#]) + let stubs = HTTPStubs(stubs: [ + HTTPStubs.stub( + url: "https://example.com/", + with: WpNetworkResponse( + body: Data(), + statusCode: 200, + headerMap: .withLinkHeader(#"; rel="https://api.w.org/""#) + ) + ), + HTTPStubs.stub( + url: "https://example.com/wp-json/", + with: WpNetworkResponse( + body: "not a json".data(using: .utf8)!, + statusCode: 200, + headerMap: .withLinkHeader(#"; rel="https://api.w.org/""#) + ) ) - ) - try stubs.stub( - url: "https://example.com/wp-json/", - with: WpNetworkResponse( - body: "not a json".data(using: .utf8)!, - statusCode: 200, - headerMap: .fromMap(hashMap: ["Link": #"; rel="https://api.w.org/""#]) - ) - ) + ]) let client = WordPressLoginClient(requestExecutor: stubs) do { @@ -116,15 +114,6 @@ class LoginTests: XCTestCase { } func testMissingAuthenticationEndpoint() async throws { - try stubs.stub( - url: "https://example.com/", - with: WpNetworkResponse( - body: Data(), - statusCode: 200, - headerMap: .fromMap(hashMap: ["Link": #"; rel="https://api.w.org/""#]) - ) - ) - let wpJsonResponse = try XCTUnwrap( Bundle.module.url( forResource: "Responses/LoginTests-wp-json-missing-authentication-endpoint", @@ -132,14 +121,24 @@ class LoginTests: XCTestCase { ) ) - try stubs.stub( - url: "https://example.com/wp-json/", - with: WpNetworkResponse( - body: Data(contentsOf: wpJsonResponse), - statusCode: 200, - headerMap: .fromMap(hashMap: ["Link": #"; rel="https://api.w.org/""#]) + let stubs = HTTPStubs(stubs: [ + HTTPStubs.stub( + url: "https://example.com/", + with: WpNetworkResponse( + body: Data(), + statusCode: 200, + headerMap: .withLinkHeader(#"; rel="https://api.w.org/""#) + ) + ), + HTTPStubs.stub( + url: "https://example.com/wp-json/", + with: WpNetworkResponse( + body: try Data(contentsOf: wpJsonResponse), + statusCode: 200, + headerMap: .withLinkHeader(#"; rel="https://api.w.org/""#) + ) ) - ) + ]) let client = WordPressLoginClient(requestExecutor: stubs) do { @@ -159,15 +158,6 @@ class LoginTests: XCTestCase { } func testRejectedResult() async throws { - try stubs.stub( - url: "https://example.com/", - with: WpNetworkResponse( - body: Data(), - statusCode: 200, - headerMap: .fromMap(hashMap: ["Link": #"; rel="https://api.w.org/""#]) - ) - ) - let wpJsonResponse = try XCTUnwrap( Bundle.module.url( forResource: "Responses/LoginTests-wp-json", @@ -175,14 +165,24 @@ class LoginTests: XCTestCase { ) ) - try stubs.stub( - url: "https://example.com/wp-json/", - with: WpNetworkResponse( - body: Data(contentsOf: wpJsonResponse), - statusCode: 200, - headerMap: .fromMap(hashMap: ["Link": #"; rel="https://api.w.org/""#]) + let stubs = HTTPStubs(stubs: [ + HTTPStubs.stub( + url: "https://example.com/", + with: WpNetworkResponse( + body: Data(), + statusCode: 200, + headerMap: .withLinkHeader(#"; rel="https://api.w.org/""#) + ) + ), + HTTPStubs.stub( + url: "https://example.com/wp-json/", + with: WpNetworkResponse( + body: try Data(contentsOf: wpJsonResponse), + statusCode: 200, + headerMap: .withLinkHeader(#"; rel="https://api.w.org/""#) + ) ) - ) + ]) let client = WordPressLoginClient(requestExecutor: stubs) let rejectedURL = try XCTUnwrap(URL(string: "x-wordpress-app://login-callback?success=false")) @@ -203,15 +203,6 @@ class LoginTests: XCTestCase { } func testApprovedResult() async throws { - try stubs.stub( - url: "https://example.com/", - with: WpNetworkResponse( - body: Data(), - statusCode: 200, - headerMap: .fromMap(hashMap: ["Link": #"; rel="https://api.w.org/""#]) - ) - ) - let wpJsonResponse = try XCTUnwrap( Bundle.module.url( forResource: "Responses/LoginTests-wp-json", @@ -219,14 +210,24 @@ class LoginTests: XCTestCase { ) ) - try stubs.stub( - url: "https://example.com/wp-json/", - with: WpNetworkResponse( - body: Data(contentsOf: wpJsonResponse), - statusCode: 200, - headerMap: .fromMap(hashMap: ["Link": #"; rel="https://api.w.org/""#]) + let stubs = HTTPStubs(stubs: [ + HTTPStubs.stub( + url: "https://example.com/", + with: WpNetworkResponse( + body: Data(), + statusCode: 200, + headerMap: .withLinkHeader(#"; rel="https://api.w.org/""#) + ) + ), + HTTPStubs.stub( + url: "https://example.com/wp-json/", + with: WpNetworkResponse( + body: try Data(contentsOf: wpJsonResponse), + statusCode: 200, + headerMap: .withLinkHeader(#"; rel="https://api.w.org/""#) + ) ) - ) + ]) let client = WordPressLoginClient(requestExecutor: stubs) // swiftlint:disable:next line_length @@ -243,7 +244,6 @@ class LoginTests: XCTestCase { XCTAssertEqual(success.userLogin, "admin") XCTAssertEqual(success.password, "123456") } - } private class Authenticator: WordPressLoginClient.Authenticator { diff --git a/native/swift/Tests/wordpress-api/Support/Extensions.swift b/native/swift/Tests/wordpress-api/Support/Extensions.swift new file mode 100644 index 000000000..9fecb4662 --- /dev/null +++ b/native/swift/Tests/wordpress-api/Support/Extensions.swift @@ -0,0 +1,14 @@ +import Foundation +import XCTest + +@testable import WordPressAPIInternal + +extension WpNetworkHeaderMap { + static var empty: WpNetworkHeaderMap { + try! WpNetworkHeaderMap.fromMap(hashMap: [:]) + } + + static func withLinkHeader(_ value: String) -> WpNetworkHeaderMap { + try! WpNetworkHeaderMap.fromMap(hashMap: ["Link": value]) + } +} diff --git a/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift b/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift index 029c70187..92e632197 100644 --- a/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift +++ b/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift @@ -2,14 +2,20 @@ import Foundation import WordPressAPI #if canImport(WordPressAPIInternal) -import WordPressAPIInternal +@preconcurrency import WordPressAPIInternal #endif -class HTTPStubs: SafeRequestExecutor { +final class HTTPStubs: SafeRequestExecutor { - var stubs: [(condition: (WpNetworkRequest) -> Bool, response: WpNetworkResponse)] = [] + typealias Stub = (condition: @Sendable (WpNetworkRequest) -> Bool, response: WpNetworkResponse) - var missingStub: Result? + private let stubs: [Stub] + private let missingStub: Result? + + init(stubs: [Stub] = [], missingStub: Result? = nil) { + self.stubs = stubs + self.missingStub = missingStub + } public func execute(_ request: WpNetworkRequest) async -> Result { if let response = stub(for: request) { @@ -28,32 +34,31 @@ class HTTPStubs: SafeRequestExecutor { } } - func stub(for request: WpNetworkRequest) -> WpNetworkResponse? { + private func stub(for request: WpNetworkRequest) -> WpNetworkResponse? { stubs.first { stub in stub.condition(request) }? .response } - func stub(url: String, with response: WpNetworkResponse) { - stubs.append(( + static func stub(url: String, with response: WpNetworkResponse) -> Stub { + ( condition: { URL(string: $0.url()) == URL(string: url) }, response: response - )) + ) } - func stub(host: String, with response: WpNetworkResponse) { - stubs.append(( + static func stub(host: String, with response: WpNetworkResponse) -> Stub { + ( condition: { URL(string: $0.url())?.host == host }, response: response - )) + ) } - func stub(path: String, with response: WpNetworkResponse) { - stubs.append(( + static func stub(path: String, with response: WpNetworkResponse) -> Stub { + ( condition: { URL(string: $0.url())?.path == path }, response: response - )) + ) } - } extension WpNetworkResponse { diff --git a/native/swift/Tests/wordpress-api/WordPressAPITests.swift b/native/swift/Tests/wordpress-api/WordPressAPITests.swift index ee5b24421..1896e317e 100644 --- a/native/swift/Tests/wordpress-api/WordPressAPITests.swift +++ b/native/swift/Tests/wordpress-api/WordPressAPITests.swift @@ -37,8 +37,9 @@ final class WordPressAPITests: XCTestCase { } } """ - let stubs = HTTPStubs() - try stubs.stub(path: "/wp-json/wp/v2/users/1", with: .json(response)) + let stubs = HTTPStubs(stubs: [ + HTTPStubs.stub(path: "/wp-json/wp/v2/users/1", with: try! .json(response)) + ]) let api = try WordPressAPI( urlSession: .shared, @@ -53,8 +54,7 @@ final class WordPressAPITests: XCTestCase { #if !os(Linux) // Skip on Linux, because `XCTExpectFailure` is unavailable on Linux func testTimeout() async throws { - let stubs = HTTPStubs() - stubs.missingStub = .failure(URLError(.timedOut)) + let stubs = HTTPStubs(stubs: [], missingStub: .failure(URLError(.timedOut))) let api = try WordPressAPI( urlSession: .shared, From c4a072a510a63841882a5293b4a1be5f15c86028 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 13 Nov 2024 19:55:56 -0700 Subject: [PATCH 3/5] Adopt swift-testing Now all tests can run on Linux! --- .../Sources/wordpress-api/LoginAPI.swift | 59 ++++++ .../Tests/wordpress-api/Endpoints/Users.swift | 9 +- .../Foundation+ExtensionsTests.swift | 12 +- .../Tests/wordpress-api/HTTPErrorTests.swift | 27 +-- .../Tests/wordpress-api/LoginTests.swift | 176 ++++++++---------- .../Tests/wordpress-api/ParsedUrlTests.swift | 39 ++-- .../wordpress-api/Support/Extensions.swift | 17 ++ .../swift/Tests/wordpress-api/UUIDTests.swift | 8 +- .../wordpress-api/WordPressAPITests.swift | 35 +--- 9 files changed, 198 insertions(+), 184 deletions(-) diff --git a/native/swift/Sources/wordpress-api/LoginAPI.swift b/native/swift/Sources/wordpress-api/LoginAPI.swift index 9d3621484..61f481b50 100644 --- a/native/swift/Sources/wordpress-api/LoginAPI.swift +++ b/native/swift/Sources/wordpress-api/LoginAPI.swift @@ -23,6 +23,65 @@ public final class WordPressLoginClient { case invalidApplicationPasswordCallback case cancelled case unknown(Swift.Error) + + func isAutodiscoveryError() -> Bool { + guard case let .invalidSiteAddress(urlDiscoveryError) = self else { + return false + } + + guard case .UrlDiscoveryFailed = urlDiscoveryError else { + return false + } + + return true + } + + var isFailedToFetchApiDetails: Bool { + guard + case let .invalidSiteAddress(urlDiscoveryError) = self, + case .UrlDiscoveryFailed(let attempts) = urlDiscoveryError + else { + return false + } + + return attempts.values.contains { state in + return switch state { + case .failure(let error): urlDiscoverErrorIsFetchApiDetailsFailed(error) + default: false + } + } + } + + var isFailedToFetchApiRoot: Bool { + guard + case let .invalidSiteAddress(urlDiscoveryError) = self, + case .UrlDiscoveryFailed(let attempts) = urlDiscoveryError + else { + return false + } + + return attempts.values.contains { state in + if case let .failure(error) = state { + return isFetchRootUrlFailedError(error) + } + + return false + } + } + + private func urlDiscoverErrorIsFetchApiDetailsFailed(_ error: UrlDiscoveryAttemptError) -> Bool { + return switch error { + case .fetchApiDetailsFailed: true + default: false + } + } + + private func isFetchRootUrlFailedError(_ error: UrlDiscoveryAttemptError) -> Bool { + return switch error { + case .fetchApiRootUrlFailed: true + default: false + } + } } private let requestExecutor: SafeRequestExecutor diff --git a/native/swift/Tests/wordpress-api/Endpoints/Users.swift b/native/swift/Tests/wordpress-api/Endpoints/Users.swift index b9718e288..54b332a14 100644 --- a/native/swift/Tests/wordpress-api/Endpoints/Users.swift +++ b/native/swift/Tests/wordpress-api/Endpoints/Users.swift @@ -1,10 +1,10 @@ import Foundation -import XCTest - +import Testing @testable import WordPressAPI -final class UsersTest: XCTestCase { +struct UsersTests { + @Test func testRetrieveUser() async throws { let response = """ { @@ -45,7 +45,6 @@ final class UsersTest: XCTestCase { executor: stubs ) let user = try await api.users.retrieveWithViewContext(userId: 1) - XCTAssertEqual(user.data.name, "User Name") + #expect(user.data.name == "User Name") } - } diff --git a/native/swift/Tests/wordpress-api/Foundation+ExtensionsTests.swift b/native/swift/Tests/wordpress-api/Foundation+ExtensionsTests.swift index 47e5a183c..38d89ace8 100644 --- a/native/swift/Tests/wordpress-api/Foundation+ExtensionsTests.swift +++ b/native/swift/Tests/wordpress-api/Foundation+ExtensionsTests.swift @@ -1,9 +1,13 @@ -import XCTest +import Foundation +import Testing import WordPressAPI -final class FoundationExtensionsTests: XCTestCase { +class FoundationExtensionsTests { - func testWordPressDateTimeParsing() throws { - XCTAssertNotNil(Date.fromWordPressDate("2024-07-04T01:49:37")) + @Test("The Foundation Extension can parse WordPress-formatted date strings", arguments: [ + "2024-07-04T01:49:37" + ]) + func testWordPressDateTimeParsing(_ string: String) throws { + #expect(Date.fromWordPressDate(string) != nil) } } diff --git a/native/swift/Tests/wordpress-api/HTTPErrorTests.swift b/native/swift/Tests/wordpress-api/HTTPErrorTests.swift index db8d0a09d..389bfecca 100644 --- a/native/swift/Tests/wordpress-api/HTTPErrorTests.swift +++ b/native/swift/Tests/wordpress-api/HTTPErrorTests.swift @@ -1,16 +1,10 @@ import Foundation -import XCTest - +import Testing @testable import WordPressAPI -#if canImport(WordPressAPIInternal) -import WordPressAPIInternal -#endif - -class HTTPErrorTests: XCTestCase { +class HTTPErrorTests { -#if !os(Linux) - // Skip on Linux, because `XCTExpectFailure` is unavailable on Linux + @Test func testTimeout() async throws { let stubs = HTTPStubs(stubs: [], missingStub: .failure(URLError(.timedOut))) @@ -21,19 +15,8 @@ class HTTPErrorTests: XCTestCase { executor: stubs ) - do { + await #expect(throws: WpApiError.self, performing: { _ = try await api.users.retrieveWithViewContext(userId: 1) - XCTFail("Unexpected response") - } catch let error as URLError { - XCTAssertEqual(error.code, .timedOut) - } catch { - #if canImport(WordPressAPIInternal) - XCTAssertTrue(error is WordPressAPIInternal.WpApiError) - #else - XCTAssertTrue(error is WpApiError) - #endif - } + }) } -#endif - } diff --git a/native/swift/Tests/wordpress-api/LoginTests.swift b/native/swift/Tests/wordpress-api/LoginTests.swift index 01c7e0156..b0cbc50e4 100644 --- a/native/swift/Tests/wordpress-api/LoginTests.swift +++ b/native/swift/Tests/wordpress-api/LoginTests.swift @@ -1,5 +1,5 @@ import Foundation -import XCTest +import Testing @testable import WordPressAPI @@ -7,65 +7,65 @@ import XCTest import WordPressAPIInternal #endif -class LoginTests: XCTestCase { +@Suite("Login") +class LoginTests { // swiftlint:disable:next force_try let appId = { try! WpUuid.parse(input: "caa8b54a-eb5e-4134-8ae2-a3946a428ec7") }() + @Test func testInvalidUrl() async { let stubs = HTTPStubs(stubs: [], missingStub: .failure(URLError(.timedOut))) + let client = WordPressLoginClient(requestExecutor: stubs) - do { - let result = await client.login( + + await #expect(performing: { + _ = try await client.login( site: "invalid url", appName: "foo", appId: appId, authenticator: Authenticator() - ) - let success = try result.get() - XCTFail("Unexpected successful result: \(success)") - } catch WordPressLoginClient.Error.invalidSiteAddress { - // Do nothing - } catch { - XCTFail("Unexpected error: \(error)") - } + ).get() + }, throws: { error in + guard let loginError = error as? WordPressLoginClient.Error, case .invalidSiteAddress = loginError else { + return false + } + + return true + }) } + @Test func testNotWordPressSite() async throws { let stubs = HTTPStubs(stubs: [ HTTPStubs.stub( host: "example.com", with: WpNetworkResponse(body: Data(), statusCode: 200, headerMap: .empty) ) - ]) + ], missingStub: .failure(URLError(.timedOut))) let client = WordPressLoginClient(requestExecutor: stubs) - do { - let result = await client.login( + + await #expect(performing: { + _ = try await client.login( site: "https://example.com/blog", appName: "foo", appId: appId, authenticator: Authenticator() - ) - let success = try result.get() - XCTFail("Unexpected successful result: \(success)") - } catch let WordPressLoginClient.Error.invalidSiteAddress(error) { - switch error { - case let .UrlDiscoveryFailed(attempts: attempts): - let notWordPressSiteError = attempts.values.contains { - if case .failure(.fetchApiRootUrlFailed) = $0 { - return true - } - return false - } - - XCTAssertTrue(notWordPressSiteError, "Error is not 'fetchApiRootUrlFailed': \(error)") + ).get() + }, throws: { error in + guard let loginError = error as? WordPressLoginClient.Error, case .invalidSiteAddress = loginError else { + return false } - } catch { - XCTFail("Unexpected error: \(error)") - } + + #expect(loginError.isAutodiscoveryError()) + #expect(loginError.isFailedToFetchApiRoot, "Error must be a `fetchApiRootUrlError` error") + + return true + }) } + @Test func testWpJsonError() async throws { let stubs = HTTPStubs(stubs: [ HTTPStubs.stub( @@ -87,39 +87,30 @@ class LoginTests: XCTestCase { ]) let client = WordPressLoginClient(requestExecutor: stubs) - do { - let result = await client.login( + + await #expect(performing: { + _ = try await client.login( site: "https://example.com", appName: "foo", appId: appId, authenticator: Authenticator() - ) - let success = try result.get() - XCTFail("Unexpected successful result: \(success)") - } catch let WordPressLoginClient.Error.invalidSiteAddress(error) { - switch error { - case let .UrlDiscoveryFailed(attempts: attempts): - let notWordPressSiteError = attempts.values.contains { - if case .failure(.fetchApiDetailsFailed) = $0 { - return true - } - return false - } - - XCTAssertTrue(notWordPressSiteError, "Error is not 'fetchApiRootUrlFailed': \(error)") + ).get() + }, throws: { error in + guard let loginError = error as? WordPressLoginClient.Error else { + return false } - } catch { - XCTFail("Unexpected error: \(error)") - } + + #expect(loginError.isAutodiscoveryError()) + #expect(loginError.isFailedToFetchApiDetails, "Error must be a `fetchApiRootUrlFailed` error") + return true + }) } func testMissingAuthenticationEndpoint() async throws { - let wpJsonResponse = try XCTUnwrap( - Bundle.module.url( - forResource: "Responses/LoginTests-wp-json-missing-authentication-endpoint", - withExtension: "json" - ) - ) + let filePath = Bundle.module.url( + forResource: "Responses/LoginTests-wp-json-missing-authentication-endpoint", + withExtension: "json" + )! let stubs = HTTPStubs(stubs: [ HTTPStubs.stub( @@ -133,7 +124,7 @@ class LoginTests: XCTestCase { HTTPStubs.stub( url: "https://example.com/wp-json/", with: WpNetworkResponse( - body: try Data(contentsOf: wpJsonResponse), + body: try Data(contentsOf: filePath), statusCode: 200, headerMap: .withLinkHeader(#"; rel="https://api.w.org/""#) ) @@ -141,29 +132,24 @@ class LoginTests: XCTestCase { ]) let client = WordPressLoginClient(requestExecutor: stubs) - do { - let result = await client.login( + + await #expect(performing: { + _ = try await client.login( site: "https://example.com", appName: "foo", appId: appId, authenticator: Authenticator() - ) - let success = try result.get() - XCTFail("Unexpected successful result: \(success)") - } catch WordPressLoginClient.Error.missingLoginUrl { - // Do nothing - } catch { - XCTFail("Unexpected error: \(error)") - } + ).get() + }, throws: { error in + error as? WordPressLoginClient.Error == .missingLoginUrl + }) } func testRejectedResult() async throws { - let wpJsonResponse = try XCTUnwrap( - Bundle.module.url( - forResource: "Responses/LoginTests-wp-json", - withExtension: "json" - ) - ) + let filePath = Bundle.module.url( + forResource: "Responses/LoginTests-wp-json", + withExtension: "json" + )! let stubs = HTTPStubs(stubs: [ HTTPStubs.stub( @@ -177,7 +163,7 @@ class LoginTests: XCTestCase { HTTPStubs.stub( url: "https://example.com/wp-json/", with: WpNetworkResponse( - body: try Data(contentsOf: wpJsonResponse), + body: try Data(contentsOf: filePath), statusCode: 200, headerMap: .withLinkHeader(#"; rel="https://api.w.org/""#) ) @@ -185,30 +171,25 @@ class LoginTests: XCTestCase { ]) let client = WordPressLoginClient(requestExecutor: stubs) - let rejectedURL = try XCTUnwrap(URL(string: "x-wordpress-app://login-callback?success=false")) - do { - let result = await client.login( + let rejectedURL = URL(string: "x-wordpress-app://login-callback?success=false")! + + await #expect(performing: { + _ = try await client.login( site: "https://example.com", appName: "foo", appId: appId, authenticator: Authenticator().returning(.success(rejectedURL)) - ) - let success = try result.get() - XCTFail("Unexpected successful result: \(success)") - } catch WordPressLoginClient.Error.authenticationError(.UnsuccessfulLogin) { - // Do nothing - } catch { - XCTFail("Unexpected error: \(error)") - } + ).get() + }, throws: { error in + error as? WordPressLoginClient.Error == .authenticationError(.UnsuccessfulLogin) + }) } func testApprovedResult() async throws { - let wpJsonResponse = try XCTUnwrap( - Bundle.module.url( - forResource: "Responses/LoginTests-wp-json", - withExtension: "json" - ) - ) + let filePath = Bundle.module.url( + forResource: "Responses/LoginTests-wp-json", + withExtension: "json" + )! let stubs = HTTPStubs(stubs: [ HTTPStubs.stub( @@ -222,7 +203,7 @@ class LoginTests: XCTestCase { HTTPStubs.stub( url: "https://example.com/wp-json/", with: WpNetworkResponse( - body: try Data(contentsOf: wpJsonResponse), + body: try Data(contentsOf: filePath), statusCode: 200, headerMap: .withLinkHeader(#"; rel="https://api.w.org/""#) ) @@ -231,7 +212,7 @@ class LoginTests: XCTestCase { let client = WordPressLoginClient(requestExecutor: stubs) // swiftlint:disable:next line_length - let successfulURL = try XCTUnwrap(URL(string: "x-wordpress-app://login-callback?site_url=https%3A%2F%2Fexample.com&user_login=admin&password=123456")) + let successfulURL = URL(string: "x-wordpress-app://login-callback?site_url=https%3A%2F%2Fexample.com&user_login=admin&password=123456")! let result = await client.login( site: "https://example.com", @@ -240,9 +221,10 @@ class LoginTests: XCTestCase { authenticator: Authenticator().returning(.success(successfulURL)) ) let success = try result.get() - XCTAssertEqual(success.siteUrl, "https://example.com") - XCTAssertEqual(success.userLogin, "admin") - XCTAssertEqual(success.password, "123456") + + #expect(success.siteUrl == "https://example.com") + #expect(success.userLogin == "admin") + #expect(success.password == "123456") } } diff --git a/native/swift/Tests/wordpress-api/ParsedUrlTests.swift b/native/swift/Tests/wordpress-api/ParsedUrlTests.swift index 44e06131f..76c55cc34 100644 --- a/native/swift/Tests/wordpress-api/ParsedUrlTests.swift +++ b/native/swift/Tests/wordpress-api/ParsedUrlTests.swift @@ -1,28 +1,23 @@ import Foundation -import XCTest +import Testing import WordPressAPI -class ParsedUrlTests: XCTestCase { +class ParsedUrlTests { - func testRoundTrip() throws { - let urls = [ - "http://example.com", - "https://www.example.com/path/to/resource", - "https://example.com/search?q=unit+testing&sort=asc", - "https://example.com/index.html#section", - "http://example.com:8080/path", - "https://subdomain.example.com", - "http://user:password@example.com", - "file:///home/user/file.txt", - "ftp://ftp.example.com/resource.txt", - "http://[2001:db8::1]:8080/" - ] - - for url in urls { - let parsedUrl = try ParsedUrl.parse(input: url) - let urlString = parsedUrl.asURL().absoluteString - try XCTAssertEqual(parsedUrl.url(), ParsedUrl.parse(input: urlString).url()) - } + @Test("URLs are parsed successfully", arguments: [ + "http://example.com/", + "https://www.example.com/path/to/resource", + "https://example.com/search?q=unit+testing&sort=asc", + "https://example.com/index.html#section", + "http://example.com:8080/path", + "https://subdomain.example.com/", + "http://user:password@example.com/", + "file:///home/user/file.txt", + "ftp://ftp.example.com/resource.txt", + "http://[2001:db8::1]:8080/" + ]) + func testRoundTrip(_ string: String) throws { + let parsedUrl = try ParsedUrl.parse(input: string) + #expect(string == parsedUrl.asURL().absoluteString) } - } diff --git a/native/swift/Tests/wordpress-api/Support/Extensions.swift b/native/swift/Tests/wordpress-api/Support/Extensions.swift index 9fecb4662..990c4e564 100644 --- a/native/swift/Tests/wordpress-api/Support/Extensions.swift +++ b/native/swift/Tests/wordpress-api/Support/Extensions.swift @@ -5,10 +5,27 @@ import XCTest extension WpNetworkHeaderMap { static var empty: WpNetworkHeaderMap { + // swiftlint:disable:next force_try try! WpNetworkHeaderMap.fromMap(hashMap: [:]) } static func withLinkHeader(_ value: String) -> WpNetworkHeaderMap { + // swiftlint:disable:next force_try try! WpNetworkHeaderMap.fromMap(hashMap: ["Link": value]) } } + +// These `Sendable` conformances are **NOT** safe – they're for the test suite only. +// +// Until or unless `WpNetworkRequest` and `WpNetworkRequest` become `uniffi::Record` (thus Structs) +// we can't guarantee that they're thread-safe + +extension WpNetworkRequest: @unchecked Sendable {} +extension WpNetworkResponse: @unchecked Sendable {} + +// This is only for testing – it's not production-ready +extension WordPressLoginClient.Error: Equatable { + public static func == (lhs: WordPressLoginClient.Error, rhs: WordPressLoginClient.Error) -> Bool { + lhs.localizedDescription == rhs.localizedDescription + } +} diff --git a/native/swift/Tests/wordpress-api/UUIDTests.swift b/native/swift/Tests/wordpress-api/UUIDTests.swift index b3b42ae4d..99a821dfe 100644 --- a/native/swift/Tests/wordpress-api/UUIDTests.swift +++ b/native/swift/Tests/wordpress-api/UUIDTests.swift @@ -1,12 +1,12 @@ import Foundation -import XCTest +import Testing import WordPressAPI -class WPUUIDTests: XCTestCase { +struct WPUUIDTests { + @Test func testConvertToUUID() { let uuid = WpUuid().uuidString() - XCTAssertNotNil(UUID(uuidString: uuid), "WpUuid \(uuid) is not a Foundation.UUID") + #expect(UUID(uuidString: uuid) != nil, "WpUuid \(uuid) is not a Foundation.UUID") } - } diff --git a/native/swift/Tests/wordpress-api/WordPressAPITests.swift b/native/swift/Tests/wordpress-api/WordPressAPITests.swift index 1896e317e..06684c28e 100644 --- a/native/swift/Tests/wordpress-api/WordPressAPITests.swift +++ b/native/swift/Tests/wordpress-api/WordPressAPITests.swift @@ -1,13 +1,14 @@ -import XCTest import Foundation +import Testing @testable import WordPressAPI #if canImport(WordPressAPIInternal) import WordPressAPIInternal #endif -final class WordPressAPITests: XCTestCase { +struct WordPressAPITests { + @Test func testExample() async throws { let response = """ { @@ -38,7 +39,7 @@ final class WordPressAPITests: XCTestCase { } """ let stubs = HTTPStubs(stubs: [ - HTTPStubs.stub(path: "/wp-json/wp/v2/users/1", with: try! .json(response)) + HTTPStubs.stub(path: "/wp-json/wp/v2/users/1", with: try .json(response)) ]) let api = try WordPressAPI( @@ -48,32 +49,6 @@ final class WordPressAPITests: XCTestCase { executor: stubs ) let user = try await api.users.retrieveWithViewContext(userId: 1) - XCTAssertEqual(user.data.name, "User Name") + #expect(user.data.name == "User Name") } - -#if !os(Linux) - // Skip on Linux, because `XCTExpectFailure` is unavailable on Linux - func testTimeout() async throws { - let stubs = HTTPStubs(stubs: [], missingStub: .failure(URLError(.timedOut))) - - let api = try WordPressAPI( - urlSession: .shared, - baseUrl: ParsedUrl.parse(input: "https://wordpress.org"), - authenticationStategy: .none, - executor: stubs - ) - - do { - _ = try await api.users.retrieveWithViewContext(userId: 1) - XCTFail("Unexpected response") - } catch let error as URLError { - XCTAssertEqual(error.code, .timedOut) - } catch { - #if canImport(WordPressAPIInternal) - XCTAssertTrue(error is WordPressAPIInternal.WpApiError) - #endif - } - } -#endif - } From 2e8dfc37132d9346534073fa4f60bdf80a876ac7 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 13 Nov 2024 20:47:51 -0700 Subject: [PATCH 4/5] =?UTF-8?q?Don=E2=80=99t=20require=20WordPressAPIInter?= =?UTF-8?q?nal=20for=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- native/swift/Sources/wordpress-api/Exports.swift | 4 ++++ native/swift/Tests/wordpress-api/LoginTests.swift | 4 ---- native/swift/Tests/wordpress-api/Support/Extensions.swift | 4 +--- native/swift/Tests/wordpress-api/Support/HTTPStubs.swift | 4 ---- native/swift/Tests/wordpress-api/WordPressAPITests.swift | 4 ---- 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/native/swift/Sources/wordpress-api/Exports.swift b/native/swift/Sources/wordpress-api/Exports.swift index ce158447d..36ceb1610 100644 --- a/native/swift/Sources/wordpress-api/Exports.swift +++ b/native/swift/Sources/wordpress-api/Exports.swift @@ -9,8 +9,12 @@ import WordPressAPIInternal public typealias WpApiError = WordPressAPIInternal.WpApiError +public typealias RequestExecutionError = WordPressAPIInternal.RequestExecutionError public typealias ParsedUrl = WordPressAPIInternal.ParsedUrl public typealias WpUuid = WordPressAPIInternal.WpUuid +public typealias WpNetworkRequest = WordPressAPIInternal.WpNetworkRequest +public typealias WpNetworkResponse = WordPressAPIInternal.WpNetworkResponse +public typealias WpNetworkHeaderMap = WordPressAPIInternal.WpNetworkHeaderMap // MARK: - Login diff --git a/native/swift/Tests/wordpress-api/LoginTests.swift b/native/swift/Tests/wordpress-api/LoginTests.swift index b0cbc50e4..cd34afa96 100644 --- a/native/swift/Tests/wordpress-api/LoginTests.swift +++ b/native/swift/Tests/wordpress-api/LoginTests.swift @@ -3,10 +3,6 @@ import Testing @testable import WordPressAPI -#if canImport(WordPressAPIInternal) -import WordPressAPIInternal -#endif - @Suite("Login") class LoginTests { diff --git a/native/swift/Tests/wordpress-api/Support/Extensions.swift b/native/swift/Tests/wordpress-api/Support/Extensions.swift index 990c4e564..a24b603dd 100644 --- a/native/swift/Tests/wordpress-api/Support/Extensions.swift +++ b/native/swift/Tests/wordpress-api/Support/Extensions.swift @@ -1,7 +1,5 @@ import Foundation -import XCTest - -@testable import WordPressAPIInternal +import WordPressAPI extension WpNetworkHeaderMap { static var empty: WpNetworkHeaderMap { diff --git a/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift b/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift index 92e632197..eaa10fab7 100644 --- a/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift +++ b/native/swift/Tests/wordpress-api/Support/HTTPStubs.swift @@ -1,10 +1,6 @@ import Foundation import WordPressAPI -#if canImport(WordPressAPIInternal) -@preconcurrency import WordPressAPIInternal -#endif - final class HTTPStubs: SafeRequestExecutor { typealias Stub = (condition: @Sendable (WpNetworkRequest) -> Bool, response: WpNetworkResponse) diff --git a/native/swift/Tests/wordpress-api/WordPressAPITests.swift b/native/swift/Tests/wordpress-api/WordPressAPITests.swift index 06684c28e..ad13ecfd3 100644 --- a/native/swift/Tests/wordpress-api/WordPressAPITests.swift +++ b/native/swift/Tests/wordpress-api/WordPressAPITests.swift @@ -2,10 +2,6 @@ import Foundation import Testing @testable import WordPressAPI -#if canImport(WordPressAPIInternal) -import WordPressAPIInternal -#endif - struct WordPressAPITests { @Test From 5bd706866f2a13694f7c514bd8a84d1e5e3b9073 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 13 Nov 2024 19:56:42 -0700 Subject: [PATCH 5/5] Remove Linux-specific code Foundation is now the same across all platforms, so we can use `URLSession.data(for:) async throws` on Linux now --- .../wordpress-api/URLSession+Linux.swift | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 native/swift/Sources/wordpress-api/URLSession+Linux.swift diff --git a/native/swift/Sources/wordpress-api/URLSession+Linux.swift b/native/swift/Sources/wordpress-api/URLSession+Linux.swift deleted file mode 100644 index 270d9afef..000000000 --- a/native/swift/Sources/wordpress-api/URLSession+Linux.swift +++ /dev/null @@ -1,28 +0,0 @@ -#if os(Linux) - -import Foundation -import FoundationNetworking - -// `URLSession.data(for:) async throws` is not available on Linux's Foundation framework. -extension URLSession { - func data(for request: URLRequest) async throws -> (Data, URLResponse) { - try await withCheckedThrowingContinuation { continuation in - let task = self.dataTask(with: request) { data, response, error in - if let error { - continuation.resume(throwing: error) - return - } - - guard let data = data, let response = response else { - continuation.resume(throwing: WordPressAPI.Errors.unableToParseResponse) - return - } - - continuation.resume(returning: (data, response)) - } - task.resume() - } - } -} - -#endif