diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorConfigurationBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorConfigurationBuilder.kt deleted file mode 100644 index 98912093b939..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorConfigurationBuilder.kt +++ /dev/null @@ -1,117 +0,0 @@ -package org.wordpress.android.ui.posts - -import org.wordpress.android.util.UrlUtils -import org.wordpress.gutenberg.model.EditorConfiguration -import org.wordpress.gutenberg.model.PostTypeDetails - -/** - * Utility object for building EditorConfiguration from settings maps. - * Eliminates duplication between GutenbergKitEditorFragment and GutenbergKitWarmupHelper. - */ -object EditorConfigurationBuilder { - /** - * Builds an EditorConfiguration from the provided settings map. - * - * @param settings The settings map containing all configuration values - * @return Configured EditorConfiguration instance - */ - fun build( - settings: Map, - ): EditorConfiguration { - val siteURL = settings.getSetting("siteURL") ?: "" - val siteApiRoot = settings.getSetting("siteApiRoot") ?: "" - val postType = settings.getSetting("postType") - ?: PostTypeDetails.post - val siteApiNamespace = settings.getStringArray("siteApiNamespace") - - return EditorConfiguration.builder( - siteURL = siteURL, - siteApiRoot = siteApiRoot, - postType = postType - ).apply { - val postId = settings.getSetting("postId") - ?.let { if (it == 0) null else it.toUInt() } - - // Post settings - setTitle(settings.getSetting("postTitle") ?: "") - setContent(settings.getSetting("postContent") ?: "") - setPostId(postId) - setPostStatus(settings.getSetting("status") ?: "draft") - - // Site settings - setSiteApiNamespace(siteApiNamespace) - setNamespaceExcludedPaths( - settings.getStringArray("namespaceExcludedPaths") - ) - setAuthHeader( - settings.getSetting("authHeader") ?: "" - ) - - // Features - setThemeStyles( - settings.getSettingOrDefault("themeStyles", false) - ) - setPlugins( - settings.getSettingOrDefault("plugins", false) - ) - setLocale(settings.getSetting("locale") ?: "en") - - // Editor asset caching configuration - configureEditorAssetCaching( - settings, siteURL, siteApiNamespace - ) - - // Cookies - setCookies( - settings.getSetting>("cookies") - ?: emptyMap() - ) - - // Network logging for debugging - setEnableNetworkLogging( - settings.getSettingOrDefault("enableNetworkLogging", false) - ) - }.build() - } - - private fun EditorConfiguration.Builder.configureEditorAssetCaching( - settings: Map, - siteURL: String, - siteApiNamespace: Array - ) { - setEnableAssetCaching(true) - - val siteHost = UrlUtils.getHost(siteURL) - val cachedHosts = if (!siteHost.isNullOrEmpty()) { - setOf("s0.wp.com", siteHost) - } else { - setOf("s0.wp.com") - } - setCachedAssetHosts(cachedHosts) - - val firstNamespace = siteApiNamespace.firstOrNull() ?: "" - val siteApiRoot = - settings.getSetting("siteApiRoot") ?: "" - if (firstNamespace.isNotEmpty() && siteApiRoot.isNotEmpty()) { - setEditorAssetsEndpoint( - "${siteApiRoot}wpcom/v2/${firstNamespace}editor-assets" - ) - } - } - - // Type-safe settings accessors - private inline fun Map.getSetting( - key: String - ): T? = this[key] as? T - - private inline fun Map.getSettingOrDefault( - key: String, default: T - ): T = getSetting(key) ?: default - - private fun Map.getStringArray( - key: String - ): Array = - getSetting>(key) - ?.asSequence()?.filterNotNull()?.toList()?.toTypedArray() - ?: emptyArray() -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt index 6c6ee25e5a86..7d9e3bc09b69 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt @@ -84,6 +84,7 @@ import org.wordpress.android.fluxc.model.PostModel import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.post.PostStatus import org.wordpress.android.fluxc.network.UserAgent +import org.wordpress.gutenberg.model.EditorConfiguration import org.wordpress.android.fluxc.network.rest.wpcom.site.PrivateAtomicCookie import org.wordpress.android.fluxc.store.AccountStore import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged @@ -208,7 +209,6 @@ import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.util.analytics.AnalyticsUtils import org.wordpress.android.util.analytics.AnalyticsUtils.BlockEditorEnabledSource import org.wordpress.android.util.config.ContactSupportFeatureConfig -import org.wordpress.android.util.config.GutenbergKitPluginsFeature import org.wordpress.android.util.config.PostConflictResolutionFeatureConfig import org.wordpress.android.util.extensions.setLiftOnScrollTargetViewIdAndRequestLayout import org.wordpress.android.util.helpers.MediaFile @@ -381,8 +381,6 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene @Inject lateinit var postConflictResolutionFeatureConfig: PostConflictResolutionFeatureConfig - @Inject lateinit var gutenbergKitPluginsFeature: GutenbergKitPluginsFeature - @Inject lateinit var activityNavigator: ActivityNavigator @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @@ -391,6 +389,7 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene @Inject lateinit var editorBloggingPromptsViewModel: EditorBloggingPromptsViewModel @Inject lateinit var editorJetpackSocialViewModel: EditorJetpackSocialViewModel @Inject lateinit var gutenbergKitNetworkLogger: GutenbergKitNetworkLogger + @Inject lateinit var gutenbergKitSettingsBuilder: GutenbergKitSettingsBuilder private lateinit var editPostNavigationViewModel: EditPostNavigationViewModel private lateinit var editPostSettingsViewModel: EditPostSettingsViewModel private lateinit var prepublishingViewModel: PrepublishingViewModel @@ -2208,36 +2207,37 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene onXpostsSettingsCapability(isXpostsCapable) } - val siteConfig = GutenbergKitSettingsBuilder.SiteConfig.fromSiteModel(siteModel) - val postConfig = GutenbergKitSettingsBuilder.PostConfig.fromPostModel( - editPostRepository.getPost() - ) - val featureConfig = GutenbergKitSettingsBuilder.FeatureConfig( - isPluginsFeatureEnabled = gutenbergKitPluginsFeature.isEnabled(), - isThemeStylesFeatureEnabled = siteSettings?.useThemeStyles ?: true, - isNetworkLoggingEnabled = AppPrefs.isTrackNetworkRequestsEnabled() - ) - val appConfig = GutenbergKitSettingsBuilder.AppConfig( - accessToken = accountStore.accessToken, - locale = perAppLocaleManager.getCurrentLocaleLanguageCode(), - cookies = editPostAuthViewModel.getCookiesForPrivateSites( - site, privateAtomicCookie - ), - accountUserId = accountStore.account.userId, - accountUserName = accountStore.account.userName, - userAgent = userAgent, - isJetpackSsoEnabled = isJetpackSsoEnabled - ) + val post = editPostRepository.getPost() + val configuration = buildEditorConfiguration(siteModel, post) - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = postConfig, - appConfig = appConfig, - featureConfig = featureConfig + return GutenbergKitEditorFragment.newInstance(configuration) + } + + private fun buildEditorConfiguration( + site: SiteModel, + post: PostImmutableModel? + ): EditorConfiguration { + val base = gutenbergKitSettingsBuilder.buildPostConfiguration( + site = site, + post = post, + accessToken = accountStore.accessToken ) - val configuration = EditorConfigurationBuilder.build(settings) - return GutenbergKitEditorFragment.newInstance(configuration) + val locale = perAppLocaleManager + .getCurrentLocaleLanguageCode() + .replace("_", "-").lowercase() + + return base.toBuilder() + .setLocale(locale) + .setCookies( + editPostAuthViewModel.getCookiesForPrivateSites( + site, privateAtomicCookie + ) + ) + .setEnableNetworkLogging( + AppPrefs.isTrackNetworkRequestsEnabled() + ) + .build() } override fun instantiateItem(container: ViewGroup, position: Int): Any { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilder.kt index d35fff16f41f..f2295b2dfca5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilder.kt @@ -1,170 +1,87 @@ package org.wordpress.android.ui.posts import android.util.Base64 -import org.wordpress.android.editor.gutenberg.GutenbergWebViewAuthorizationData import org.wordpress.android.fluxc.model.PostImmutableModel import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.fluxc.network.UserAgent -import org.wordpress.android.fluxc.utils.extensions.getPasswordProcessed -import org.wordpress.android.fluxc.utils.extensions.getUserNameProcessed import org.wordpress.android.util.AppLog -import org.wordpress.android.util.UrlUtils +import org.wordpress.gutenberg.model.EditorConfiguration import org.wordpress.gutenberg.model.PostTypeDetails - -object GutenbergKitSettingsBuilder { - private const val AUTH_BEARER_PREFIX = "Bearer " - private const val AUTH_BASIC_PREFIX = "Basic " - - data class SiteConfig( - val url: String, - val siteId: Long, - val isWPCom: Boolean, - val isWPComAtomic: Boolean, - val isJetpackConnected: Boolean, - val isUsingWpComRestApi: Boolean, - val wpApiRestUrl: String?, - val apiRestUsernamePlain: String?, - val apiRestPasswordPlain: String?, - val selfHostedSiteId: Long, - val webEditor: String?, - val apiRestUsernameProcessed: String?, - val apiRestPasswordProcessed: String? - ) { - companion object { - fun fromSiteModel(site: SiteModel): SiteConfig { - return SiteConfig( - url = site.url, - siteId = site.siteId, - isWPCom = site.isWPCom, - isWPComAtomic = site.isWPComAtomic, - isJetpackConnected = site.isJetpackConnected, - isUsingWpComRestApi = site.isUsingWpComRestApi, - wpApiRestUrl = site.wpApiRestUrl, - apiRestUsernamePlain = site.apiRestUsernamePlain, - apiRestPasswordPlain = site.apiRestPasswordPlain, - selfHostedSiteId = site.selfHostedSiteId, - webEditor = site.webEditor, - apiRestUsernameProcessed = site.getUserNameProcessed(), - apiRestPasswordProcessed = site.getPasswordProcessed() - ) - } - } - } - - data class PostConfig( - val remotePostId: Long?, - val isPage: Boolean, - val title: String?, - val content: String?, - val status: String? - ) { - companion object { - fun fromPostModel(postModel: PostImmutableModel?): PostConfig { - return PostConfig( - remotePostId = postModel?.remotePostId, - isPage = postModel?.isPage ?: false, - title = postModel?.title, - content = postModel?.content, - status = postModel?.status - ) - } +import java.net.URI +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GutenbergKitSettingsBuilder @Inject constructor( + private val editorCapabilityResolver: EditorCapabilityResolver, +) { + fun buildPostConfiguration( + site: SiteModel, + post: PostImmutableModel? = null, + accessToken: String? + ): EditorConfiguration { + val applicationPassword = site.apiRestPasswordPlain + val shouldUseWPComRestApi = + applicationPassword.isNullOrEmpty() && site.isUsingWpComRestApi + + val siteApiRoot = if (shouldUseWPComRestApi) { + WPCOM_API_ROOT + } else { + site.wpApiRestUrl ?: "${site.url}/wp-json/" } - } - - data class FeatureConfig( - val isPluginsFeatureEnabled: Boolean, - val isThemeStylesFeatureEnabled: Boolean, - val isNetworkLoggingEnabled: Boolean = false - ) - - data class AppConfig( - val accessToken: String?, - val locale: String, - val cookies: Any?, - val accountUserId: Long, - val accountUserName: String?, - val userAgent: UserAgent, - val isJetpackSsoEnabled: Boolean - ) - - data class GutenbergKitConfig( - val siteConfig: SiteConfig, - val postConfig: PostConfig, - val appConfig: AppConfig, - val featureConfig: FeatureConfig - ) - - /** - * Builds the settings configuration for GutenbergKit editor. - * - * This method determines the appropriate authentication method based on site type: - * - WP.com sites use Bearer token authentication with the public API - * - Jetpack/self-hosted sites with application passwords use Basic authentication - * - Falls back to WP.com REST API when no application password is available - */ - fun buildSettings( - siteConfig: SiteConfig, - postConfig: PostConfig, - appConfig: AppConfig, - featureConfig: FeatureConfig - ): MutableMap { - val applicationPassword = siteConfig.apiRestPasswordPlain - val shouldUseWPComRestApi = applicationPassword.isNullOrEmpty() && siteConfig.isUsingWpComRestApi - - val siteApiRoot = if (shouldUseWPComRestApi) "https://public-api.wordpress.com/" - else siteConfig.wpApiRestUrl ?: "${siteConfig.url}/wp-json/" val authHeader = buildAuthHeader( shouldUseWPComRestApi = shouldUseWPComRestApi, - accessToken = appConfig.accessToken, - username = siteConfig.apiRestUsernamePlain, + accessToken = accessToken, + username = site.apiRestUsernamePlain, password = applicationPassword - ) - - val siteApiNamespace = if (shouldUseWPComRestApi) - arrayOf("sites/${siteConfig.siteId}/", "sites/${UrlUtils.removeScheme(siteConfig.url)}/") - else arrayOf() + ) ?: "" - val wpcomLocaleSlug = appConfig.locale.replace("_", "-").lowercase() - - return mutableMapOf( - "postId" to postConfig.remotePostId?.toInt(), - "postType" to if (postConfig.isPage) { - PostTypeDetails.page - } else { - PostTypeDetails.post - }, - "status" to postConfig.status, - "postTitle" to postConfig.title, - "postContent" to postConfig.content, - "siteURL" to siteConfig.url, - "siteApiRoot" to siteApiRoot, - "namespaceExcludedPaths" to arrayOf("/wpcom/v2/following/recommendations", "/wpcom/v2/following/mine"), - "authHeader" to authHeader, - "siteApiNamespace" to siteApiNamespace, - "themeStyles" to featureConfig.isThemeStylesFeatureEnabled, - "plugins" to shouldUsePlugins( - isFeatureEnabled = featureConfig.isPluginsFeatureEnabled, - isWPComSite = siteConfig.isWPCom, - isJetpackConnected = siteConfig.isJetpackConnected, - applicationPassword = applicationPassword - ), - "locale" to wpcomLocaleSlug, - "cookies" to appConfig.cookies, - "enableNetworkLogging" to featureConfig.isNetworkLoggingEnabled + val siteApiNamespace = buildSiteApiNamespace( + shouldUseWPComRestApi, site.siteId, site.url ) + + val postType = if (post?.isPage == true) PostTypeDetails.page else PostTypeDetails.post + + val cachedHosts = buildCachedHosts(site.url) + val editorAssetsEndpoint = + buildEditorAssetsEndpoint(siteApiRoot, siteApiNamespace) + + return EditorConfiguration.builder( + siteURL = site.url, + siteApiRoot = siteApiRoot, + postType = postType + ).apply { + setTitle(post?.title ?: "") + setContent(post?.content ?: "") + setPostId( + if (post?.isLocalDraft == true) null + else post?.remotePostId?.toUInt() + ) + setPostStatus(post?.status ?: "draft") + setAuthHeader(authHeader) + setSiteApiNamespace(siteApiNamespace) + setNamespaceExcludedPaths( + arrayOf( + "/wpcom/v2/following/recommendations", + "/wpcom/v2/following/mine" + ) + ) + setThemeStyles( + editorCapabilityResolver.resolveThemeStyles(site).shouldApplyInEditor + ) + setPlugins( + editorCapabilityResolver.resolveThirdPartyBlocks(site).shouldApplyInEditor + ) + setLocale("en") + setCookies(emptyMap()) + setEnableAssetCaching(true) + setCachedAssetHosts(cachedHosts) + setEditorAssetsEndpoint(editorAssetsEndpoint) + setEnableNetworkLogging(false) + }.build() } - /** - * Builds the authentication header based on the authentication method. - * - * @param shouldUseWPComRestApi True if using WP.com REST API (Bearer auth) - * @param accessToken The OAuth2 access token for WP.com authentication - * @param username The username for Basic auth (application passwords) - * @param password The password for Basic auth (application passwords) - * @return The formatted authentication header string, or null if credentials are invalid - */ - private fun buildAuthHeader( + fun buildAuthHeader( shouldUseWPComRestApi: Boolean, accessToken: String?, username: String?, @@ -174,7 +91,10 @@ object GutenbergKitSettingsBuilder { if (!accessToken.isNullOrEmpty()) { "$AUTH_BEARER_PREFIX$accessToken" } else { - AppLog.w(AppLog.T.EDITOR, "Missing access token for WP.com REST API authentication") + AppLog.w( + AppLog.T.EDITOR, + "Missing access token for WP.com REST API authentication" + ) null } } else { @@ -187,49 +107,67 @@ object GutenbergKitSettingsBuilder { ) "$AUTH_BASIC_PREFIX$encodedCredentials" } catch (e: IllegalArgumentException) { - AppLog.e(AppLog.T.EDITOR, "Failed to encode Basic auth credentials", e) + AppLog.e( + AppLog.T.EDITOR, + "Failed to encode Basic auth credentials", + e + ) null } } else { - AppLog.w(AppLog.T.EDITOR, "Incomplete credentials for Basic authentication") + AppLog.w( + AppLog.T.EDITOR, + "Incomplete credentials for Basic authentication" + ) null } } } - private fun shouldUsePlugins( - isFeatureEnabled: Boolean, - isWPComSite: Boolean, - isJetpackConnected: Boolean, - applicationPassword: String? - ): Boolean { - // Enable plugins for: - // 1. WP.com Simple sites (when feature is enabled) - // 2. Jetpack-connected sites with application passwords (when feature is enabled) - return isFeatureEnabled && - (isWPComSite || (isJetpackConnected && !applicationPassword.isNullOrEmpty())) + internal fun buildSiteApiNamespace( + shouldUseWPComRestApi: Boolean, + siteId: Long, + siteUrl: String + ): Array { + if (!shouldUseWPComRestApi) return arrayOf() + val host = extractHost(siteUrl) + return if (host != null) { + arrayOf("sites/$siteId/", "sites/$host/") + } else { + arrayOf("sites/$siteId/") + } } - /** - * Builds Gutenberg WebView authorization data for the fragment. - */ - fun buildAuthorizationData( - siteConfig: SiteConfig, - appConfig: AppConfig - ): GutenbergWebViewAuthorizationData { - return GutenbergWebViewAuthorizationData( - siteConfig.url, - siteConfig.isWPCom || siteConfig.isWPComAtomic, - appConfig.accountUserId, - appConfig.accountUserName, - appConfig.accessToken, - siteConfig.selfHostedSiteId, - siteConfig.apiRestUsernameProcessed, - siteConfig.apiRestPasswordProcessed, - siteConfig.isUsingWpComRestApi, - siteConfig.webEditor, - appConfig.userAgent.webViewUserAgent, - appConfig.isJetpackSsoEnabled - ) + private fun buildCachedHosts(siteUrl: String): Set { + val siteHost = extractHost(siteUrl) + return if (!siteHost.isNullOrEmpty()) { + setOf("s0.wp.com", siteHost) + } else { + setOf("s0.wp.com") + } + } + + private fun buildEditorAssetsEndpoint( + siteApiRoot: String, + siteApiNamespace: Array + ): String? { + if (siteApiRoot.isEmpty()) return null + val firstNamespace = siteApiNamespace.firstOrNull() ?: "" + return "${siteApiRoot}wpcom/v2/${firstNamespace}editor-assets" + } + + internal fun extractHost(url: String): String? { + return try { + URI(url).host + } catch (_: Exception) { + null + } + } + + companion object { + private const val AUTH_BEARER_PREFIX = "Bearer " + private const val AUTH_BASIC_PREFIX = "Basic " + private const val WPCOM_API_ROOT = + "https://public-api.wordpress.com/" } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilderTest.kt index de5ab603c1e8..4e0a8e7ad5c0 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilderTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilderTest.kt @@ -1,688 +1,640 @@ package org.wordpress.android.ui.posts -import android.content.Context import org.assertj.core.api.Assertions.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner -import org.wordpress.android.fluxc.network.UserAgent +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever +import org.wordpress.android.fluxc.model.PostModel +import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.gutenberg.model.PostTypeDetails @RunWith(MockitoJUnitRunner::class) -@Suppress("LargeClass") class GutenbergKitSettingsBuilderTest { - // ===== Plugin Logic Tests ===== @Mock - lateinit var appContext: Context + lateinit var editorCapabilityResolver: EditorCapabilityResolver - @Test - fun `plugins disabled when feature flag is off regardless of site configuration`() { - val testCases = listOf( - // isWPCom, isJetpackConnected, applicationPassword - Triple(true, false, null), // WPCom site - Triple(false, true, "password"), // Jetpack with password - Triple(false, false, null), // Self-hosted - ) + private val builder by lazy { + GutenbergKitSettingsBuilder(editorCapabilityResolver) + } - testCases.forEach { (isWPCom, isJetpack, password) -> - val siteConfig = createSiteConfig( - isWPCom = isWPCom, - isJetpackConnected = isJetpack, - apiRestPasswordPlain = password - ) + @Before + fun setUp() { + whenever(editorCapabilityResolver.resolveThemeStyles(any())) + .thenReturn(Resolved.Hidden) + whenever(editorCapabilityResolver.resolveThirdPartyBlocks(any())) + .thenReturn(Resolved.Hidden) + } - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(), + // ===== Auth Header Tests ===== - featureConfig = createFeatureConfig(), // Both features disabled - ) + @Test + fun `WPCom site returns Bearer token header`() { + val header = builder.buildAuthHeader( + shouldUseWPComRestApi = true, + accessToken = "my_token", + username = null, + password = null + ) - assertThat(settings["plugins"]) - .withFailMessage("Expected plugins=false for WPCom=$isWPCom, Jetpack=$isJetpack, password=$password") - .isEqualTo(false) - } + assertThat(header).isEqualTo("Bearer my_token") } @Test - fun `plugins enabled for WPCom sites when feature flag is on`() { - val siteConfig = createSiteConfig(isWPCom = true) - - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(), + fun `WPCom site with null token returns null`() { + val header = builder.buildAuthHeader( + shouldUseWPComRestApi = true, + accessToken = null, + username = null, + password = null + ) - featureConfig = createFeatureConfig(isPluginsFeatureEnabled = true), + assertThat(header).isNull() + } + @Test + fun `WPCom site with empty token returns null`() { + val header = builder.buildAuthHeader( + shouldUseWPComRestApi = true, + accessToken = "", + username = null, + password = null ) - assertThat(settings["plugins"]).isEqualTo(true) + assertThat(header).isNull() } @Test - fun `plugins enabled for Jetpack sites with application password when feature flag is on`() { - val siteConfig = createSiteConfig( - isWPCom = false, - isJetpackConnected = true, - apiRestPasswordPlain = "validPassword123" + fun `self-hosted site returns Basic auth header`() { + val header = builder.buildAuthHeader( + shouldUseWPComRestApi = false, + accessToken = null, + username = "testuser", + password = "testpass" ) - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(), - - featureConfig = createFeatureConfig(isPluginsFeatureEnabled = true), + assertThat(header).isNotNull() + assertThat(header).startsWith("Basic ") + } + @Test + fun `Basic auth with null username returns null`() { + val header = builder.buildAuthHeader( + shouldUseWPComRestApi = false, + accessToken = null, + username = null, + password = "password123" ) - assertThat(settings["plugins"]).isEqualTo(true) + assertThat(header).isNull() } @Test - fun `plugins disabled for Jetpack sites without application password`() { - val passwordVariants = listOf(null, "") + fun `Basic auth with empty username returns null`() { + val header = builder.buildAuthHeader( + shouldUseWPComRestApi = false, + accessToken = null, + username = "", + password = "password123" + ) - passwordVariants.forEach { password -> - val siteConfig = createSiteConfig( - isWPCom = false, - isJetpackConnected = true, - apiRestPasswordPlain = password - ) + assertThat(header).isNull() + } - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(), + @Test + fun `Basic auth with null password returns null`() { + val header = builder.buildAuthHeader( + shouldUseWPComRestApi = false, + accessToken = null, + username = "username", + password = null + ) - featureConfig = createFeatureConfig(isPluginsFeatureEnabled = true), + assertThat(header).isNull() + } - ) + @Test + fun `Basic auth with empty password returns null`() { + val header = builder.buildAuthHeader( + shouldUseWPComRestApi = false, + accessToken = null, + username = "username", + password = "" + ) - assertThat(settings["plugins"]) - .withFailMessage("Expected plugins=false for password=$password") - .isEqualTo(false) - } + assertThat(header).isNull() } @Test - fun `plugins disabled for self-hosted sites without Jetpack`() { - val siteConfig = createSiteConfig( - isWPCom = false, - isJetpackConnected = false, - apiRestPasswordPlain = "password" // Has password but no Jetpack + fun `Basic auth with both empty returns null`() { + val header = builder.buildAuthHeader( + shouldUseWPComRestApi = false, + accessToken = null, + username = "", + password = "" ) - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(), - - featureConfig = createFeatureConfig(isPluginsFeatureEnabled = true), + assertThat(header).isNull() + } + @Test + fun `special characters in Basic auth are encoded`() { + val header = builder.buildAuthHeader( + shouldUseWPComRestApi = false, + accessToken = null, + username = "user@example.com", + password = "p@ss:word!123" ) - assertThat(settings["plugins"]).isEqualTo(false) + assertThat(header).isNotNull() + assertThat(header).startsWith("Basic ") } - // ===== Authentication Flow Tests ===== + // ===== Site API Namespace Tests ===== @Test - fun `WPCom site uses Bearer token and public API`() { - val siteConfig = createSiteConfig( - url = "https://example.wordpress.com", - siteId = 123, - isWPCom = true, - isUsingWpComRestApi = true + fun `namespace is empty for non-WPCom sites`() { + val result = builder.buildSiteApiNamespace( + shouldUseWPComRestApi = false, + siteId = 123L, + siteUrl = "https://example.com" ) - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(accessToken = "test_bearer_token"), - - featureConfig = createFeatureConfig(), + assertThat(result).isEmpty() + } + @Test + fun `namespace includes site ID and host for WPCom sites`() { + val result = builder.buildSiteApiNamespace( + shouldUseWPComRestApi = true, + siteId = 456L, + siteUrl = "https://example.wordpress.com" ) - assertThat(settings["authHeader"]).isEqualTo("Bearer test_bearer_token") - assertThat(settings["siteApiRoot"]).isEqualTo("https://public-api.wordpress.com/") - assertThat(settings["siteApiNamespace"] as Array<*>) - .containsExactly("sites/123/", "sites/example.wordpress.com/") + assertThat(result).containsExactly( + "sites/456/", + "sites/example.wordpress.com/" + ) } @Test - fun `Jetpack site with application password uses Basic auth and site API`() { - val siteConfig = createSiteConfig( - url = "https://mysite.com", - siteId = 789, - isJetpackConnected = true, - wpApiRestUrl = "https://mysite.com/wp-json/", - apiRestUsernamePlain = "testuser", - apiRestPasswordPlain = "testpass123" + fun `namespace includes only site ID when host extraction fails`() { + val result = builder.buildSiteApiNamespace( + shouldUseWPComRestApi = true, + siteId = 789L, + siteUrl = "not-a-valid-url" ) - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(accessToken = "unused_token"), + assertThat(result).containsExactly("sites/789/") + } - featureConfig = createFeatureConfig(), + // ===== Extract Host Tests ===== - ) + @Test + fun `extractHost returns host from valid URL`() { + assertThat( + builder.extractHost( + "https://example.wordpress.com" + ) + ).isEqualTo("example.wordpress.com") + } - assertThat(settings["authHeader"] as String).startsWith("Basic ") - assertThat(settings["siteApiRoot"]).isEqualTo("https://mysite.com/wp-json/") - assertThat(settings["siteApiNamespace"] as Array<*>).isEmpty() + @Test + fun `extractHost returns null for invalid URL`() { + assertThat( + builder.extractHost("not-a-url") + ).isNull() } @Test - fun `Jetpack site without password falls back to Bearer when WPCom REST available`() { - val siteConfig = createSiteConfig( - isJetpackConnected = true, - isUsingWpComRestApi = true, - apiRestPasswordPlain = null - ) + fun `extractHost strips path from URL`() { + assertThat( + builder.extractHost( + "https://example.com/blog/page" + ) + ).isEqualTo("example.com") + } - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(accessToken = "fallback_token"), + // ===== buildPostConfiguration Tests ===== - featureConfig = createFeatureConfig(), + // --- WPCom site configuration --- - ) + @Test + fun `WPCom site uses WPCom API root`() { + val config = buildWPComConfig() - assertThat(settings["authHeader"]).isEqualTo("Bearer fallback_token") - assertThat(settings["siteApiRoot"]).isEqualTo("https://public-api.wordpress.com/") + assertThat(config.siteApiRoot) + .isEqualTo("https://public-api.wordpress.com/") } - // ===== Authentication Edge Cases Tests ===== + @Test + fun `WPCom site sets Bearer auth header`() { + val config = buildWPComConfig(accessToken = "wpcom_token") + + assertThat(config.authHeader) + .isEqualTo("Bearer wpcom_token") + } @Test - fun `WPCom site with null access token returns null auth header`() { - val siteConfig = createSiteConfig( - isWPCom = true, - isUsingWpComRestApi = true + fun `WPCom site sets site API namespace with ID and host`() { + val config = buildWPComConfig( + siteUrl = "https://mysite.wordpress.com", + siteId = 42L ) - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(accessToken = null), - featureConfig = createFeatureConfig() + assertThat(config.siteApiNamespace).containsExactly( + "sites/42/", + "sites/mysite.wordpress.com/" ) - - assertThat(settings["authHeader"]).isNull() } @Test - fun `WPCom site with empty access token returns null auth header`() { - val siteConfig = createSiteConfig( - isWPCom = true, - isUsingWpComRestApi = true - ) + fun `WPCom site sets editor assets endpoint`() { + val config = buildWPComConfig(siteId = 100L) - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(accessToken = ""), - featureConfig = createFeatureConfig() + assertThat(config.editorAssetsEndpoint).isEqualTo( + "https://public-api.wordpress.com/" + + "wpcom/v2/sites/100/editor-assets" ) + } + + @Test + fun `WPCom site with missing token uses empty auth header`() { + val config = buildWPComConfig(accessToken = null) - assertThat(settings["authHeader"]).isNull() + assertThat(config.authHeader).isEmpty() } + // --- Self-hosted site configuration --- + @Test - fun `Basic auth with null username returns null auth header`() { - val siteConfig = createSiteConfig( - isJetpackConnected = true, - apiRestUsernamePlain = null, - apiRestPasswordPlain = "password123" + fun `self-hosted site uses wpApiRestUrl as API root`() { + val config = buildSelfHostedConfig( + wpApiRestUrl = "https://mysite.com/wp-json/" ) - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(), - featureConfig = createFeatureConfig() + assertThat(config.siteApiRoot) + .isEqualTo("https://mysite.com/wp-json/") + } + + @Test + fun `self-hosted site falls back to siteUrl wp-json when no REST URL`() { + val config = buildSelfHostedConfig( + siteUrl = "https://mysite.com", + wpApiRestUrl = null ) - assertThat(settings["authHeader"]).isNull() + assertThat(config.siteApiRoot) + .isEqualTo("https://mysite.com/wp-json/") } @Test - fun `Basic auth with empty username returns null auth header`() { - val siteConfig = createSiteConfig( - isJetpackConnected = true, - apiRestUsernamePlain = "", - apiRestPasswordPlain = "password123" + fun `self-hosted site sets Basic auth header`() { + val config = buildSelfHostedConfig( + applicationPassword = "app_pass", + apiRestUsername = "admin" ) - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(), - featureConfig = createFeatureConfig() - ) + assertThat(config.authHeader).startsWith("Basic ") + } + + @Test + fun `self-hosted site has empty namespace`() { + val config = buildSelfHostedConfig() - assertThat(settings["authHeader"]).isNull() + assertThat(config.siteApiNamespace).isEmpty() } @Test - fun `Basic auth with null password returns null auth header`() { - val siteConfig = createSiteConfig( - isJetpackConnected = true, - apiRestUsernamePlain = "username", - apiRestPasswordPlain = null + fun `self-hosted site builds editor assets endpoint from API root`() { + val config = buildSelfHostedConfig() + + assertThat(config.editorAssetsEndpoint).isEqualTo( + "https://mysite.com/wp-json/wpcom/v2/editor-assets" ) + } + + // --- Application password overrides WPCom REST API --- - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(), - featureConfig = createFeatureConfig() + @Test + fun `app password forces non-WPCom API even if site uses WPCom REST`() { + val site = SiteModel().apply { + url = "https://mysite.com" + siteId = 123L + setIsWPCom(false) + setIsJetpackConnected(true) + origin = SiteModel.ORIGIN_WPCOM_REST + wpApiRestUrl = "https://mysite.com/wp-json/" + apiRestPasswordPlain = "app_pass" + apiRestUsernamePlain = "admin" + } + val config = builder.buildPostConfiguration( + site = site, + accessToken = "wpcom_token" ) - assertThat(settings["authHeader"]).isNull() + assertThat(config.siteApiRoot) + .isEqualTo("https://mysite.com/wp-json/") + assertThat(config.authHeader).startsWith("Basic ") + assertThat(config.siteApiNamespace).isEmpty() } + // --- Post configuration --- + @Test - fun `Basic auth with empty password returns null auth header`() { - val siteConfig = createSiteConfig( - isJetpackConnected = true, - apiRestUsernamePlain = "username", - apiRestPasswordPlain = "" - ) + fun `post type is post by default`() { + val config = buildWPComConfig() - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(), - featureConfig = createFeatureConfig() - ) + assertThat(config.postType).isEqualTo(PostTypeDetails.post) + } - assertThat(settings["authHeader"]).isNull() + @Test + fun `null post title becomes empty string`() { + val config = buildWPComConfig() + + assertThat(config.title).isEmpty() } @Test - fun `Basic auth with both username and password empty returns null auth header`() { - val siteConfig = createSiteConfig( - isJetpackConnected = true, - apiRestUsernamePlain = "", - apiRestPasswordPlain = "" - ) + fun `null post content becomes empty string`() { + val config = buildWPComConfig() - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(), - featureConfig = createFeatureConfig() - ) + assertThat(config.content).isEmpty() + } - assertThat(settings["authHeader"]).isNull() + @Test + fun `null remote ID results in null post ID`() { + val config = buildWPComConfig() + + assertThat(config.postId).isNull() } @Test - fun `Valid WPCom authentication returns proper Bearer header`() { - val siteConfig = createSiteConfig( - isWPCom = true, - isUsingWpComRestApi = true + fun `local draft post results in null post ID`() { + val site = SiteModel().apply { + url = "https://example.wordpress.com" + siteId = 123L + setIsWPCom(true) + setIsJetpackConnected(false) + origin = SiteModel.ORIGIN_WPCOM_REST + } + val post = PostModel().apply { + setIsLocalDraft(true) + setRemotePostId(99L) + } + val config = builder.buildPostConfiguration( + site = site, + post = post, + accessToken = "test_token" ) - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(accessToken = "valid_token_123"), - featureConfig = createFeatureConfig() - ) + assertThat(config.postId).isNull() + } + + @Test + fun `null post status defaults to draft`() { + val config = buildWPComConfig() + + assertThat(config.postStatus).isEqualTo("draft") + } + + // --- Asset caching --- + + @Test + fun `asset caching is always enabled`() { + val config = buildWPComConfig() - assertThat(settings["authHeader"]).isEqualTo("Bearer valid_token_123") + assertThat(config.enableAssetCaching).isTrue() } @Test - fun `Valid Basic auth returns proper Basic header`() { - val siteConfig = createSiteConfig( - isJetpackConnected = true, - apiRestUsernamePlain = "testuser", - apiRestPasswordPlain = "testpass" + fun `cached hosts includes s0 wp com and site host`() { + val config = buildWPComConfig( + siteUrl = "https://mysite.wordpress.com" ) - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(), - featureConfig = createFeatureConfig() + assertThat(config.cachedAssetHosts).containsExactlyInAnyOrder( + "s0.wp.com", + "mysite.wordpress.com" ) + } + + @Test + fun `cached hosts includes only s0 wp com for invalid URL`() { + val config = buildWPComConfig(siteUrl = "not-a-url") - val authHeader = settings["authHeader"] as String? - assertThat(authHeader).isNotNull() - assertThat(authHeader).startsWith("Basic ") - // Verify it's a valid Base64 encoded string - val encodedPart = authHeader?.removePrefix("Basic ") - assertThat(encodedPart).isNotEmpty() + assertThat(config.cachedAssetHosts) + .containsExactly("s0.wp.com") } + // --- Namespace excluded paths --- + @Test - fun `Special characters in Basic auth credentials are handled correctly`() { - val siteConfig = createSiteConfig( - isJetpackConnected = true, - apiRestUsernamePlain = "user@example.com", - apiRestPasswordPlain = "p@ss:word!123" - ) + fun `namespace excluded paths are always set`() { + val config = buildWPComConfig() - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(), - featureConfig = createFeatureConfig() + assertThat(config.namespaceExcludedPaths).containsExactly( + "/wpcom/v2/following/recommendations", + "/wpcom/v2/following/mine" ) - - val authHeader = settings["authHeader"] as String? - assertThat(authHeader).isNotNull() - assertThat(authHeader).startsWith("Basic ") } - // ===== Complete Scenario Tests ===== + // --- Site URL passthrough --- @Test - fun `complete settings for WPCom simple site with all features enabled`() { - val siteConfig = GutenbergKitSettingsBuilder.SiteConfig( - url = "https://example.wordpress.com", - siteId = 123, - isWPCom = true, - isWPComAtomic = false, - isJetpackConnected = false, - isUsingWpComRestApi = true, - wpApiRestUrl = null, - apiRestUsernamePlain = null, - apiRestPasswordPlain = null, - selfHostedSiteId = 0, - webEditor = "gutenberg", - apiRestUsernameProcessed = null, - apiRestPasswordProcessed = null + fun `site URL is passed through to configuration`() { + val config = buildWPComConfig( + siteUrl = "https://example.wordpress.com" ) - val postConfig = GutenbergKitSettingsBuilder.PostConfig( - remotePostId = 456L, - isPage = false, - title = "Test Post", - content = "Test Content", - status = "publish" + assertThat(config.siteURL) + .isEqualTo("https://example.wordpress.com") + } + + // ===== buildCachedHosts (via buildPostConfiguration) ===== + + @Test + fun `cached hosts includes site host for subdirectory URL`() { + val config = buildWPComConfig( + siteUrl = "https://example.com/blog" ) - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = postConfig, - appConfig = createAppConfig( - accessToken = "test_token", - cookies = "test_cookies" - ), - featureConfig = createFeatureConfig( - isPluginsFeatureEnabled = true, - isThemeStylesFeatureEnabled = true - ) + assertThat(config.cachedAssetHosts).containsExactlyInAnyOrder( + "s0.wp.com", + "example.com" ) + } - // Verify all settings are correctly configured - assertThat(settings["postId"]).isEqualTo(456) - assertThat(settings["postType"]).isEqualTo(PostTypeDetails.post) - assertThat(settings["postTitle"]).isEqualTo("Test Post") - assertThat(settings["postContent"]).isEqualTo("Test Content") - assertThat(settings["siteURL"]).isEqualTo("https://example.wordpress.com") - assertThat(settings["authHeader"]).isEqualTo("Bearer test_token") - assertThat(settings["siteApiRoot"]).isEqualTo("https://public-api.wordpress.com/") - assertThat(settings["plugins"]).isEqualTo(true) // WPCom with feature enabled - assertThat(settings["themeStyles"]).isEqualTo(true) - assertThat(settings["locale"]).isEqualTo("en-us") - assertThat(settings["cookies"]).isEqualTo("test_cookies") - } - - @Test - fun `complete settings for Jetpack site with application password`() { - val siteConfig = GutenbergKitSettingsBuilder.SiteConfig( - url = "https://jetpack-site.com", - siteId = 999, - isWPCom = false, - isWPComAtomic = false, - isJetpackConnected = true, - isUsingWpComRestApi = false, - wpApiRestUrl = "https://jetpack-site.com/wp-json/", - apiRestUsernamePlain = "admin", - apiRestPasswordPlain = "securepass", - selfHostedSiteId = 999, - webEditor = "gutenberg", - apiRestUsernameProcessed = "admin", - apiRestPasswordProcessed = "securepass" - ) - - val postConfig = GutenbergKitSettingsBuilder.PostConfig( - remotePostId = 100L, - isPage = true, - title = "Test Page", - content = "Page Content", - status = "draft" - ) - - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = postConfig, - appConfig = createAppConfig( - accessToken = "unused", - locale = "fr_FR" - ), - featureConfig = createFeatureConfig(isPluginsFeatureEnabled = true) - ) - - assertThat(settings["postType"]).isEqualTo(PostTypeDetails.page) - assertThat(settings["authHeader"] as String).startsWith("Basic ") - assertThat(settings["siteApiRoot"]).isEqualTo("https://jetpack-site.com/wp-json/") - assertThat(settings["siteApiNamespace"] as Array<*>).isEmpty() - assertThat(settings["plugins"]).isEqualTo(true) // Jetpack with password and feature enabled - assertThat(settings["locale"]).isEqualTo("fr-fr") - } - - @Test - fun `locale transformation handles underscores correctly`() { - val testCases = mapOf( - "en_US" to "en-us", - "fr_FR" to "fr-fr", - "de_DE" to "de-de", - "es_ES" to "es-es", - "pt_BR" to "pt-br" - ) - - testCases.forEach { (input, expected) -> - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = createSiteConfig(), - postConfig = createPostConfig(), - appConfig = createAppConfig(locale = input), - featureConfig = createFeatureConfig() - ) + @Test + fun `cached hosts only includes s0 wp com for empty URL`() { + val config = buildWPComConfig(siteUrl = "") - assertThat(settings["locale"]) - .withFailMessage("Expected $input to transform to $expected") - .isEqualTo(expected) - } + assertThat(config.cachedAssetHosts) + .containsExactly("s0.wp.com") } + // ===== buildEditorAssetsEndpoint (via buildPostConfiguration) ===== + @Test - fun `feature flags control themeStyles and plugins independently`() { - val siteConfig = createSiteConfig(isWPCom = true) + fun `editor assets endpoint uses first namespace`() { + val config = buildWPComConfig(siteId = 55L) - // Test all combinations - val flagCombinations = listOf( - Triple(false, false, Pair(false, false)), - Triple(false, true, Pair(false, true)), - Triple(true, false, Pair(true, false)), - Triple(true, true, Pair(true, true)) + assertThat(config.editorAssetsEndpoint).isEqualTo( + "https://public-api.wordpress.com/" + + "wpcom/v2/sites/55/editor-assets" ) + } - flagCombinations.forEach { (plugins, themes, expected) -> - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(), - - featureConfig = createFeatureConfig( - isPluginsFeatureEnabled = plugins, - isThemeStylesFeatureEnabled = themes - ), - ) + @Test + fun `editor assets endpoint for non-WPCom site uses API root`() { + val config = buildSelfHostedConfig() - assertThat(settings["plugins"]).isEqualTo(expected.first) - assertThat(settings["themeStyles"]).isEqualTo(expected.second) - } + assertThat(config.editorAssetsEndpoint).isEqualTo( + "https://mysite.com/wp-json/wpcom/v2/editor-assets" + ) } + // ===== buildSiteApiNamespace edge cases ===== + @Test - fun `self-hosted site uses correct API endpoint when wpApiRestUrl is null`() { - val siteConfig = createSiteConfig( - url = "https://selfhosted.org", - wpApiRestUrl = null, - apiRestPasswordPlain = "password" + fun `namespace with empty URL returns only site ID`() { + val result = builder.buildSiteApiNamespace( + shouldUseWPComRestApi = true, + siteId = 321L, + siteUrl = "" ) - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = createPostConfig(), - appConfig = createAppConfig(), + assertThat(result).containsExactly("sites/321/") + } - featureConfig = createFeatureConfig(), + // ===== Post type and ID edge cases ===== + @Test + fun `page post results in page post type`() { + val site = SiteModel().apply { + url = "https://example.wordpress.com" + siteId = 123L + setIsWPCom(true) + setIsJetpackConnected(false) + origin = SiteModel.ORIGIN_WPCOM_REST + } + val post = PostModel().apply { + setIsPage(true) + } + val config = builder.buildPostConfiguration( + site = site, + post = post, + accessToken = "test_token" ) - assertThat(settings["siteApiRoot"]).isEqualTo("https://selfhosted.org/wp-json/") + assertThat(config.postType).isEqualTo(PostTypeDetails.page) } @Test - fun `namespaceExcludedPaths is always included`() { - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = createSiteConfig(), - postConfig = createPostConfig(), - appConfig = createAppConfig(), + fun `published post sets remote post ID`() { + val site = SiteModel().apply { + url = "https://example.wordpress.com" + siteId = 123L + setIsWPCom(true) + setIsJetpackConnected(false) + origin = SiteModel.ORIGIN_WPCOM_REST + } + val post = PostModel().apply { + setIsLocalDraft(false) + setRemotePostId(42L) + } + val config = builder.buildPostConfiguration( + site = site, + post = post, + accessToken = "test_token" + ) - featureConfig = createFeatureConfig(), + assertThat(config.postId).isEqualTo(42u) + } - ) + // ===== Capability resolver integration ===== - val excludedPaths = settings["namespaceExcludedPaths"] as Array<*> - assertThat(excludedPaths).containsExactly( - "/wpcom/v2/following/recommendations", - "/wpcom/v2/following/mine" - ) + @Test + fun `themeStyles reflects resolver result`() { + whenever(editorCapabilityResolver.resolveThemeStyles(any())) + .thenReturn(Resolved.Available(userEnabled = true)) + + val config = buildWPComConfig() + + assertThat(config.themeStyles).isTrue() } @Test - fun `null post data is handled correctly`() { - val postConfig = GutenbergKitSettingsBuilder.PostConfig( - remotePostId = null, - isPage = false, - title = null, - content = null, - status = null - ) + fun `themeStyles is false when resolver hides the capability`() { + whenever(editorCapabilityResolver.resolveThemeStyles(any())) + .thenReturn(Resolved.Hidden) - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = createSiteConfig(), - postConfig = postConfig, - appConfig = createAppConfig(), + val config = buildWPComConfig() - featureConfig = createFeatureConfig(), + assertThat(config.themeStyles).isFalse() + } - ) + @Test + fun `plugins reflects resolver result`() { + whenever(editorCapabilityResolver.resolveThirdPartyBlocks(any())) + .thenReturn(Resolved.Available(userEnabled = true)) + + val config = buildWPComConfig() - assertThat(settings["postId"]).isNull() - assertThat(settings["postTitle"]).isNull() - assertThat(settings["postContent"]).isNull() - assertThat(settings["status"]).isNull() - assertThat(settings["postType"]).isEqualTo(PostTypeDetails.post) // Still defaults to post + assertThat(config.plugins).isTrue() } @Test - fun `post status is included in settings`() { - val testCases = listOf("draft", "publish", "pending", "private", "future", "trash") + fun `plugins is false when resolver hides the capability`() { + whenever(editorCapabilityResolver.resolveThirdPartyBlocks(any())) + .thenReturn(Resolved.Hidden) - testCases.forEach { status -> - val postConfig = createPostConfig(status = status) + val config = buildWPComConfig() - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = createSiteConfig(), - postConfig = postConfig, - appConfig = createAppConfig(), - featureConfig = createFeatureConfig() - ) + assertThat(config.plugins).isFalse() + } - assertThat(settings["status"]) - .withFailMessage("Expected status=$status in settings") - .isEqualTo(status) + // ===== Helpers ===== + + private fun buildWPComConfig( + siteUrl: String = "https://example.wordpress.com", + siteId: Long = 123L, + accessToken: String? = "test_token" + ): org.wordpress.gutenberg.model.EditorConfiguration { + val site = SiteModel().apply { + url = siteUrl + this.siteId = siteId + setIsWPCom(true) + setIsJetpackConnected(false) + origin = SiteModel.ORIGIN_WPCOM_REST } + return builder.buildPostConfiguration( + site = site, + accessToken = accessToken + ) } - // ===== Helper Methods ===== - - private fun createFeatureConfig( - isPluginsFeatureEnabled: Boolean = false, - isThemeStylesFeatureEnabled: Boolean = false - ) = GutenbergKitSettingsBuilder.FeatureConfig( - isPluginsFeatureEnabled = isPluginsFeatureEnabled, - isThemeStylesFeatureEnabled = isThemeStylesFeatureEnabled - ) - - private fun createAppConfig( - accessToken: String? = "token", - locale: String = "en_US", - cookies: Any? = null - ) = GutenbergKitSettingsBuilder.AppConfig( - accessToken = accessToken, - locale = locale, - cookies = cookies, - accountUserId = 123L, - accountUserName = "testuser", - userAgent = UserAgent(appContext = appContext, appName = "foo"), - isJetpackSsoEnabled = false - ) - - private fun createSiteConfig( - url: String = "https://test.com", - siteId: Long = 1, - isWPCom: Boolean = false, - isWPComAtomic: Boolean = false, - isJetpackConnected: Boolean = false, - isUsingWpComRestApi: Boolean = false, - wpApiRestUrl: String? = null, - apiRestUsernamePlain: String? = null, - apiRestPasswordPlain: String? = null - ) = GutenbergKitSettingsBuilder.SiteConfig( - url = url, - siteId = siteId, - isWPCom = isWPCom, - isWPComAtomic = isWPComAtomic, - isJetpackConnected = isJetpackConnected, - isUsingWpComRestApi = isUsingWpComRestApi, - wpApiRestUrl = wpApiRestUrl, - apiRestUsernamePlain = apiRestUsernamePlain, - apiRestPasswordPlain = apiRestPasswordPlain, - selfHostedSiteId = siteId, - webEditor = "gutenberg", - apiRestUsernameProcessed = apiRestUsernamePlain, - apiRestPasswordProcessed = apiRestPasswordPlain - ) - - private fun createPostConfig( - remotePostId: Long? = 1L, - isPage: Boolean = false, - title: String? = "Test", - content: String? = "Content", - status: String? = "draft" - ) = GutenbergKitSettingsBuilder.PostConfig( - remotePostId = remotePostId, - isPage = isPage, - title = title, - content = content, - status = status - ) + private fun buildSelfHostedConfig( + siteUrl: String = "https://mysite.com", + wpApiRestUrl: String? = "https://mysite.com/wp-json/", + applicationPassword: String? = "app_pass", + apiRestUsername: String? = "admin" + ): org.wordpress.gutenberg.model.EditorConfiguration { + val site = SiteModel().apply { + url = siteUrl + siteId = 999L + setIsWPCom(false) + setIsJetpackConnected(false) + this.wpApiRestUrl = wpApiRestUrl + apiRestPasswordPlain = applicationPassword + apiRestUsernamePlain = apiRestUsername + } + return builder.buildPostConfiguration( + site = site, + accessToken = null + ) + } }