Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ class RESTAPIRepository(
private val json = Json { ignoreUnknownKeys = true }

private val apiRoot = configuration.siteApiRoot.trimEnd('/')
private val namespace = configuration.siteApiNamespace.firstOrNull()
private val namespace = configuration.siteApiNamespace.firstOrNull()?.let {
it.trimEnd('/') + "/"
}
private val editorSettingsUrl = buildNamespacedUrl(EDITOR_SETTINGS_PATH)
private val activeThemeUrl = buildNamespacedUrl(ACTIVE_THEME_PATH)
private val siteSettingsUrl = buildNamespacedUrl(SITE_SETTINGS_PATH)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,15 @@ class RESTAPIRepositoryTest {

private fun makeConfiguration(
shouldUsePlugins: Boolean = true,
shouldUseThemeStyles: Boolean = true
shouldUseThemeStyles: Boolean = true,
siteApiRoot: String = TEST_API_ROOT,
siteApiNamespace: Array<String> = arrayOf()
): EditorConfiguration {
return EditorConfiguration.builder(TEST_SITE_URL, TEST_API_ROOT, "post")
return EditorConfiguration.builder(TEST_SITE_URL, siteApiRoot, "post")
.setPlugins(shouldUsePlugins)
.setThemeStyles(shouldUseThemeStyles)
.setAuthHeader("Bearer test-token")
.setSiteApiNamespace(siteApiNamespace)
.build()
}

Expand Down Expand Up @@ -337,6 +340,33 @@ class RESTAPIRepositoryTest {
assertEquals(expectedURLs, capturedURLs.toSet())
}

@Test
fun `namespace is inserted into URLs`() = runBlocking {
val capturedURLs = mutableListOf<String>()
val capturingClient = createCapturingClient { capturedURLs.add(it) }
val configuration = makeConfiguration(siteApiNamespace = arrayOf("sites/123/"))
val repository = makeRepository(configuration = configuration, httpClient = capturingClient)

repository.fetchPost(id = 1)
repository.fetchEditorSettings()
repository.fetchSettingsOptions()

assertTrue(capturedURLs.any { it.contains("sites/123/posts/1") })
assertTrue(capturedURLs.any { it.contains("sites/123/settings") })
}

@Test
fun `namespace without trailing slash is normalized`() = runBlocking {
val capturedURLs = mutableListOf<String>()
val capturingClient = createCapturingClient { capturedURLs.add(it) }
val configuration = makeConfiguration(siteApiNamespace = arrayOf("sites/123"))
val repository = makeRepository(configuration = configuration, httpClient = capturingClient)

repository.fetchPost(id = 1)

assertTrue(capturedURLs.any { it.contains("sites/123/posts/1") })
}

private fun createCapturingClient(onRequest: (String) -> Unit): EditorHTTPClientProtocol {
return object : EditorHTTPClientProtocol {
override suspend fun download(url: String, destination: File): EditorHTTPClientDownloadResponse {
Expand Down
4 changes: 3 additions & 1 deletion ios/Sources/GutenbergKit/Sources/RESTAPIRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ public struct RESTAPIRepository: Sendable {
/// Builds a URL by inserting the namespace after the version segment of the path.
/// For example: `/wp/v2/posts` with namespace `sites/123/` becomes `/wp/v2/sites/123/posts`
private static func buildNamespacedURL(apiRoot: URL, path: String, namespace: String?) -> URL {
guard let namespace = namespace else {
guard let rawNamespace = namespace else {
return apiRoot.appending(rawPath: path)
}

let namespace = rawNamespace.hasSuffix("/") ? rawNamespace : rawNamespace + "/"

// Parse the path to find where to insert the namespace
// Path format is typically: /prefix/version/endpoint (e.g., /wp/v2/posts or /wp-block-editor/v1/settings)
let components = path.split(separator: "/", omittingEmptySubsequences: true)
Expand Down
28 changes: 28 additions & 0 deletions ios/Tests/GutenbergKitTests/Services/RESTAPIRepositoryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,33 @@ struct RESTAPIRepositoryTests: MakesTestFixtures {
#expect(capturedURL?.absoluteString.contains("context=edit") == true)
#expect(capturedURL?.absoluteString.contains("/posts/42") == true)
}

@Test("namespace is inserted into URLs")
func namespaceIsInsertedIntoURLs() async throws {
let mockClient = EditorAssetLibraryMockHTTPClient()
let configuration = makeConfiguration(siteApiNamespace: ["sites/123/"])
let repository = makeRepository(configuration: configuration, httpClient: mockClient)

_ = try await repository.fetchPost(id: 1)
_ = try await repository.fetchEditorSettings()
_ = try await repository.fetchSettingsOptions()
Copy link
Copy Markdown
Member

@dcalhoun dcalhoun Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These invocations and others in the files caused JSON decoding errors due to the incomplete mock. This caused test and CI task failures. I'm pushed 0222eb3, but feel free to revert or change the approach as desired.


let urls = mockClient.requestedURLs.map(\.absoluteString)
#expect(urls.contains { $0.contains("sites/123/posts/1") })
#expect(urls.contains { $0.contains("sites/123/settings") })
}

@Test("namespace without trailing slash is normalized")
func namespaceWithoutTrailingSlashIsNormalized() async throws {
let mockClient = EditorAssetLibraryMockHTTPClient()
let configuration = makeConfiguration(siteApiNamespace: ["sites/123"])
let repository = makeRepository(configuration: configuration, httpClient: mockClient)

_ = try await repository.fetchPost(id: 1)

let urls = mockClient.requestedURLs.map(\.absoluteString)
#expect(urls.contains { $0.contains("sites/123/posts/1") })
}
}

// MARK: - URL Capturing Mock Client
Expand All @@ -286,3 +313,4 @@ final class URLCapturingMockHTTPClient: EditorHTTPClientProtocol, @unchecked Sen
)
}
}

8 changes: 5 additions & 3 deletions ios/Tests/GutenbergKitTests/TestHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ protocol MakesTestFixtures {

func makeConfiguration(
postID: Int?, title: String?, content: String?, siteURL: URL, postType: PostTypeDetails,
shouldUsePlugins: Bool, shouldUseThemeStyles: Bool
shouldUsePlugins: Bool, shouldUseThemeStyles: Bool, siteApiNamespace: [String]
) -> EditorConfiguration
func makeConfigurationBuilder(postType: PostTypeDetails) -> EditorConfigurationBuilder
func makeService(for configuration: EditorConfiguration?) -> EditorService
Expand All @@ -40,12 +40,14 @@ extension MakesTestFixtures {
siteURL: URL = Self.testSiteURL,
postType: PostTypeDetails = .post,
shouldUsePlugins: Bool = true,
shouldUseThemeStyles: Bool = true
shouldUseThemeStyles: Bool = true,
siteApiNamespace: [String] = []
) -> EditorConfiguration {
var builder = EditorConfigurationBuilder(
postType: postType,
siteURL: siteURL,
siteApiRoot: Self.testApiRoot
siteApiRoot: Self.testApiRoot,
siteApiNamespace: siteApiNamespace
)
.apply(title, { $0.setTitle($1) })
.apply(content, { $0.setContent($1) })
Expand Down
Loading