diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index da4c1fefe..09ec7766a 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -468,7 +468,13 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro // it because the WebView has no auth cookies). Without both there is nothing // to upload through, so leave the server down and let uploads fall to the // default WebView path rather than start a server that could only fail. - guard !configuration.authHeader.isEmpty else { + // + // `MediaServerCredentials` owns the check so it is reachable from the host + // test suite — this file is not. + guard MediaServerCredentials.areUsable( + siteApiRoot: configuration.siteApiRoot, + authHeader: configuration.authHeader + ) else { return } diff --git a/ios/Sources/GutenbergKit/Sources/Media/MediaServerCredentials.swift b/ios/Sources/GutenbergKit/Sources/Media/MediaServerCredentials.swift new file mode 100644 index 000000000..a8ce4ee51 --- /dev/null +++ b/ios/Sources/GutenbergKit/Sources/Media/MediaServerCredentials.swift @@ -0,0 +1,24 @@ +import Foundation + +/// Whether the editor configuration can reach the configured site for media. +/// +/// Deliberately outside `EditorViewController`. That type is `#if canImport(UIKit)`, +/// so on the macOS host it does not exist and nothing in it can be tested — including +/// this check, which already diverged silently between iOS and Android once. Living +/// here, it is reachable from the host test suite. +enum MediaServerCredentials { + /// Whether a ``DefaultMediaUploader`` built from this configuration could actually + /// reach the site. + /// + /// Both fields are required. The uploader delivers GutenbergKit's uploads to the + /// configured site, so it needs somewhere to send them and credentials to be + /// accepted; with either missing, every media request it makes fails. + /// + /// `siteApiRoot` is a `URL` here where Android types it as a `String`, so the + /// equivalent of Android's `isEmpty()` check is "not absolute" — a URL with no + /// scheme or host cannot address the site, and every request built from it fails at + /// the URLSession layer. + static func areUsable(siteApiRoot: URL, authHeader: String) -> Bool { + siteApiRoot.scheme != nil && siteApiRoot.host() != nil && !authHeader.isEmpty + } +} diff --git a/ios/Tests/GutenbergKitTests/Media/MediaServerCredentialsTests.swift b/ios/Tests/GutenbergKitTests/Media/MediaServerCredentialsTests.swift new file mode 100644 index 000000000..b5b39e349 --- /dev/null +++ b/ios/Tests/GutenbergKitTests/Media/MediaServerCredentialsTests.swift @@ -0,0 +1,45 @@ +import Foundation +import Testing + +@testable import GutenbergKit + +@Suite("MediaServerCredentials") +struct MediaServerCredentialsTests { + private static let siteRoot = URL(string: "https://example.com/wp-json/")! + + @Test("accepts an absolute site root with an auth header") + func acceptsUsableCredentials() { + #expect(MediaServerCredentials.areUsable(siteApiRoot: Self.siteRoot, authHeader: "Bearer t")) + } + + @Test("rejects an empty auth header") + func rejectsEmptyAuthHeader() { + #expect(!MediaServerCredentials.areUsable(siteApiRoot: Self.siteRoot, authHeader: "")) + } + + // The two arms below are what a `URL` makes different from Android's `String`: + // `isEmpty()` has no direct equivalent, so "addressable" is spelled as scheme and + // host both being present. A URL missing either cannot reach the site, and every + // request built from it fails at the URLSession layer. + + @Test("rejects a site root with no scheme") + func rejectsSchemelessSiteRoot() { + // A network-path reference: it has a host, so only the scheme arm rejects it. + // `example.com/wp-json/` would not do — it parses as a bare path with no host + // either, and passes on the host arm alone. + let relative = URL(string: "//example.com/wp-json/")! + #expect(relative.host() != nil) + #expect(!MediaServerCredentials.areUsable(siteApiRoot: relative, authHeader: "Bearer t")) + } + + @Test("rejects a site root with no host") + func rejectsHostlessSiteRoot() { + let fileURL = URL(fileURLWithPath: "/tmp/wp-json") + #expect(!MediaServerCredentials.areUsable(siteApiRoot: fileURL, authHeader: "Bearer t")) + } + + @Test("rejects a site root with no scheme or host") + func rejectsRelativeSiteRoot() { + #expect(!MediaServerCredentials.areUsable(siteApiRoot: URL(string: "/")!, authHeader: "Bearer t")) + } +}