diff --git a/android-core/proguard.pro b/android-core/proguard.pro index 400222184..956b9a715 100644 --- a/android-core/proguard.pro +++ b/android-core/proguard.pro @@ -84,6 +84,10 @@ -keep class com.mparticle.MPEvent$* { *; } -keep class com.mparticle.MParticle { *; } +-keep class com.mparticle.MParticle$Internal { *; } +-keep class com.mparticle.internal.ConfigManager { + public com.mparticle.networking.NetworkOptions getNetworkOptions(); +} -keep class com.mparticle.MParticle$EventType { *; } -keep class com.mparticle.MParticle$InstallType { *; } -keep class com.mparticle.MParticle$IdentityType { *; } diff --git a/android-core/src/androidTest/kotlin/com.mparticle/networking/MParticleBaseClientImplTest.kt b/android-core/src/androidTest/kotlin/com.mparticle/networking/MParticleBaseClientImplTest.kt index 349a4faa4..d74f9f768 100644 --- a/android-core/src/androidTest/kotlin/com.mparticle/networking/MParticleBaseClientImplTest.kt +++ b/android-core/src/androidTest/kotlin/com.mparticle/networking/MParticleBaseClientImplTest.kt @@ -422,4 +422,177 @@ class MParticleBaseClientImplTest : BaseCleanInstallEachTest() { ) assertEquals(null, result) } + + @Test + @Throws(MalformedURLException::class) + fun testCustomBaseURLConfigEndpoint() { + val options = + MParticleOptions + .builder(mContext) + .credentials(apiKey, "secret") + .networkOptions( + NetworkOptions + .builder() + .setCustomBaseURL("https://rkt.example.com") + .build(), + ).build() + MParticle.start(options) + val baseClientImpl = AccessUtils.getApiClient() as MParticleBaseClientImpl + val configUrl = baseClientImpl.getUrl(MParticleBaseClientImpl.Endpoint.CONFIG) + Assert.assertTrue(configUrl.toString().contains("rkt.example.com/config/v4/")) + Assert.assertFalse(configUrl.toString().contains("config2.mparticle.com")) + } + + @Test + @Throws(MalformedURLException::class) + fun testCustomBaseURLEventsEndpoint() { + val options = + MParticleOptions + .builder(mContext) + .credentials(apiKey, "secret") + .networkOptions( + NetworkOptions + .builder() + .setCustomBaseURL("https://rkt.example.com") + .build(), + ).build() + MParticle.start(options) + val uploadSettings = UploadSettings(apiKey, "secret", options.networkOptions, "", "") + val baseClientImpl = AccessUtils.getApiClient() as MParticleBaseClientImpl + val eventsUrl = + baseClientImpl.getUrl(MParticleBaseClientImpl.Endpoint.EVENTS, null, null, uploadSettings) + Assert.assertTrue(eventsUrl.toString().contains("rkt.example.com/nativeevents/v2/")) + Assert.assertFalse(eventsUrl.toString().contains("nativesdks")) + } + + @Test + @Throws(MalformedURLException::class) + fun testCustomBaseURLIdentityEndpoint() { + val options = + MParticleOptions + .builder(mContext) + .credentials(apiKey, "secret") + .networkOptions( + NetworkOptions + .builder() + .setCustomBaseURL("https://rkt.example.com") + .build(), + ).build() + MParticle.start(options) + val baseClientImpl = AccessUtils.getApiClient() as MParticleBaseClientImpl + val identityUrl = + baseClientImpl.getUrl(MParticleBaseClientImpl.Endpoint.IDENTITY, "login") + Assert.assertTrue(identityUrl.toString().contains("rkt.example.com/identity/v1/login")) + Assert.assertFalse(identityUrl.toString().contains("identity.us1.mparticle.com")) + } + + @Test + @Throws(MalformedURLException::class) + fun testCustomBaseURLAliasEndpoint() { + val options = + MParticleOptions + .builder(mContext) + .credentials(apiKey, "secret") + .networkOptions( + NetworkOptions + .builder() + .setCustomBaseURL("https://rkt.example.com") + .build(), + ).build() + MParticle.start(options) + val uploadSettings = UploadSettings(apiKey, "secret", options.networkOptions, "", "") + val baseClientImpl = AccessUtils.getApiClient() as MParticleBaseClientImpl + val aliasUrl = + baseClientImpl.getUrl(MParticleBaseClientImpl.Endpoint.ALIAS, null, null, uploadSettings) + Assert.assertTrue(aliasUrl.toString().contains("rkt.example.com/nativeevents/v1/identity/")) + Assert.assertTrue(aliasUrl.toString().endsWith("/alias")) + Assert.assertFalse(aliasUrl.toString().contains("nativesdks")) + } + + @Test + @Throws(MalformedURLException::class) + fun testCustomBaseURLAudienceEndpoint() { + val options = + MParticleOptions + .builder(mContext) + .credentials(apiKey, "secret") + .networkOptions( + NetworkOptions + .builder() + .setCustomBaseURL("https://rkt.example.com") + .build(), + ).build() + MParticle.start(options) + val baseClientImpl = AccessUtils.getApiClient() as MParticleBaseClientImpl + val audienceUrl = + baseClientImpl.getUrl(MParticleBaseClientImpl.Endpoint.AUDIENCE, 12345L) + Assert.assertTrue(audienceUrl.toString().contains("rkt.example.com/nativeevents/v1/")) + Assert.assertTrue(audienceUrl.toString().contains("/audience")) + Assert.assertFalse(audienceUrl.toString().contains("nativesdks")) + } + + @Test + fun testCustomBaseURLRejectsNonHTTPS() { + val opts = + NetworkOptions + .builder() + .setCustomBaseURL("http://rkt.example.com") + .build() + assertEquals(null, opts.customBaseURL) + } + + @Test + fun testCustomBaseURLStripsPath() { + val opts = + NetworkOptions + .builder() + .setCustomBaseURL("https://rkt.example.com/some/path?q=1") + .build() + assertEquals("rkt.example.com", opts.customBaseURL) + } + + @Test + fun testCustomBaseURLPreservesPort() { + val opts = + NetworkOptions + .builder() + .setCustomBaseURL("https://rkt.example.com:8443") + .build() + assertEquals("rkt.example.com:8443", opts.customBaseURL) + } + + @Test + fun testCustomBaseURLRejectsMalformed() { + val opts = + NetworkOptions + .builder() + .setCustomBaseURL("not a url") + .build() + assertEquals(null, opts.customBaseURL) + } + + @Test + fun testCustomBaseURLSurvivesJsonRoundTrip() { + val original = + NetworkOptions + .builder() + .setCustomBaseURL("https://rkt.example.com:8443") + .build() + Assert.assertEquals("rkt.example.com:8443", original.customBaseURL) + + val json = original.toJson().toString() + val restored = NetworkOptions.withNetworkOptions(json) + Assert.assertNotNull(restored) + Assert.assertEquals("rkt.example.com:8443", restored!!.customBaseURL) + } + + @Test + fun testCustomBaseURLOmittedFromJsonWhenUnset() { + val opts = NetworkOptions.builder().build() + val json = opts.toJson() + Assert.assertFalse(json.has("customBaseURL")) + + val restored = NetworkOptions.withNetworkOptions(json.toString()) + Assert.assertNull(restored!!.customBaseURL) + } } diff --git a/android-core/src/main/java/com/mparticle/networking/MParticleBaseClientImpl.java b/android-core/src/main/java/com/mparticle/networking/MParticleBaseClientImpl.java index 2ed7817ba..cbb2153e5 100644 --- a/android-core/src/main/java/com/mparticle/networking/MParticleBaseClientImpl.java +++ b/android-core/src/main/java/com/mparticle/networking/MParticleBaseClientImpl.java @@ -114,38 +114,26 @@ protected MPUrl getUrl(Endpoint endpoint, @Nullable String identityPath, HashMap protected MPUrl getUrl(Endpoint endpoint, @Nullable String identityPath,HashMap audienceQueryParams, @Nullable UploadSettings uploadSettings) throws MalformedURLException { NetworkOptions networkOptions = uploadSettings == null ? mConfigManager.getNetworkOptions() : uploadSettings.getNetworkOptions(); DomainMapping domainMapping = networkOptions.getDomain(endpoint); - String url = NetworkOptionsManager.getDefaultUrl(endpoint); String apiKey = uploadSettings == null ? mApiKey : uploadSettings.getApiKey(); + final boolean usingCustomBaseURL = !MPUtility.isEmpty(networkOptions.getCustomBaseURL()); - // `defaultDomain` variable is for URL generation when domain mapping is specified. - String defaultDomain = url; - boolean isDefaultDomain = true; - - // Check if domain mapping is specified and update the URL based on domain mapping - String domainMappingUrl = domainMapping != null ? domainMapping.getUrl() : null; - if (!MPUtility.isEmpty(domainMappingUrl)) { - isDefaultDomain = url.equals(domainMappingUrl); - url = domainMappingUrl; - } - - if (endpoint != Endpoint.CONFIG) { - // Set URL with pod prefix if it’s the default domain and endpoint is not CONFIG - if (isDefaultDomain) { - url = getPodUrl(url, mConfigManager.getPodPrefix(apiKey), mConfigManager.isDirectUrlRoutingEnabled()); - } else { - // When domain mapping is specified, generate the default domain. Whether podRedirection is enabled or not, always use the original URL. - defaultDomain = getPodUrl(defaultDomain, null, false); - } - } + ResolvedHost host = resolveHost(endpoint, networkOptions, domainMapping, apiKey); + String url = host.url; + String defaultDomain = host.defaultDomain; + boolean isDefaultDomain = host.isDefaultDomain; Uri uri; String subdirectory; String pathPrefix; String pathPostfix; boolean overridesSubdirectory = domainMapping != null && domainMapping.isOverridesSubdirectory(); + if (usingCustomBaseURL && overridesSubdirectory) { + Logger.warning("NetworkOptions: customBaseURL with overridesSubdirectory is unsupported for CDN routing; overridesSubdirectory will be ignored for " + endpoint.name() + "."); + overridesSubdirectory = false; + } switch (endpoint) { case CONFIG: - pathPrefix = SERVICE_VERSION_4 + "/"; + pathPrefix = usingCustomBaseURL ? "/config/v4/" : SERVICE_VERSION_4 + "/"; subdirectory = overridesSubdirectory ? "" : pathPrefix; pathPostfix = mApiKey + "/config"; Uri.Builder builder = new Uri.Builder() @@ -165,9 +153,9 @@ protected MPUrl getUrl(Endpoint endpoint, @Nullable String identityPath,HashMap< } } } - return MPUrl.getUrl(builder.build().toString(), generateDefaultURL(isDefaultDomain, builder.build(), defaultDomain, (pathPrefix + pathPostfix))); + return MPUrl.getUrl(builder.build().toString(), generateDefaultURL(isDefaultDomain, builder.build(), defaultDomain, (SERVICE_VERSION_4 + "/" + pathPostfix))); case EVENTS: - pathPrefix = SERVICE_VERSION_2 + "/"; + pathPrefix = usingCustomBaseURL ? "/nativeevents/v2/" : SERVICE_VERSION_2 + "/"; subdirectory = overridesSubdirectory ? "" : pathPrefix; pathPostfix = apiKey + "/events"; uri = new Uri.Builder() @@ -176,9 +164,9 @@ protected MPUrl getUrl(Endpoint endpoint, @Nullable String identityPath,HashMap< .path(subdirectory + pathPostfix) .build(); - return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, (pathPrefix + pathPostfix))); + return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, (SERVICE_VERSION_2 + "/" + pathPostfix))); case ALIAS: - pathPrefix = SERVICE_VERSION_1 + "/identity/"; + pathPrefix = usingCustomBaseURL ? "/nativeevents/v1/identity/" : SERVICE_VERSION_1 + "/identity/"; subdirectory = overridesSubdirectory ? "" : pathPrefix; pathPostfix = apiKey + "/alias"; uri = new Uri.Builder() @@ -186,26 +174,28 @@ protected MPUrl getUrl(Endpoint endpoint, @Nullable String identityPath,HashMap< .encodedAuthority(url) .path(subdirectory + pathPostfix) .build(); - return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, (pathPrefix + pathPostfix))); + return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, (SERVICE_VERSION_1 + "/identity/" + pathPostfix))); case IDENTITY: - pathPrefix = SERVICE_VERSION_1 + "/"; - subdirectory = overridesSubdirectory ? "" : SERVICE_VERSION_1 + "/"; + pathPrefix = usingCustomBaseURL ? "/identity/v1/" : SERVICE_VERSION_1 + "/"; + subdirectory = overridesSubdirectory ? "" : pathPrefix; pathPostfix = identityPath; uri = new Uri.Builder() .scheme(BuildConfig.SCHEME) .encodedAuthority(url) .path(subdirectory + pathPostfix) .build(); - return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, (pathPrefix + pathPostfix))); + return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, (SERVICE_VERSION_1 + "/" + pathPostfix))); case AUDIENCE: - pathPostfix = SERVICE_VERSION_1 + "/" + mApiKey + "/audience"; + pathPostfix = usingCustomBaseURL + ? "/nativeevents/v1/" + mApiKey + "/audience" + : SERVICE_VERSION_1 + "/" + mApiKey + "/audience"; uri = new Uri.Builder() .scheme(BuildConfig.SCHEME) .encodedAuthority(url) .path(pathPostfix) .appendQueryParameter("mpid", String.valueOf(mConfigManager.getMpid())) .build(); - return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, pathPostfix)); + return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, (SERVICE_VERSION_1 + "/" + mApiKey + "/audience"))); default: return null; } @@ -246,6 +236,57 @@ String getPodUrl(String URLPrefix, String pod, boolean enablePodRedirection) { return null; } + /** + * Resolves the host(s) used to build the endpoint URL. Returns three values: + * {@code url} — host used for the request, {@code defaultDomain} — host used as the + * fallback for {@link #generateDefaultURL}, and {@code isDefaultDomain} — true when + * {@code url} is the unmodified mParticle default. + * + *

Priority: {@code customBaseURL} → per-endpoint {@code DomainMapping} → default. + */ + private ResolvedHost resolveHost(Endpoint endpoint, NetworkOptions networkOptions, DomainMapping domainMapping, String apiKey) { + String defaultUrl = NetworkOptionsManager.getDefaultUrl(endpoint); + String customBaseURL = networkOptions.getCustomBaseURL(); + + if (!MPUtility.isEmpty(customBaseURL)) { + if (domainMapping != null && !MPUtility.isEmpty(domainMapping.getUrl())) { + Logger.warning("NetworkOptions: customBaseURL is set; domain mapping for " + endpoint.name() + " is ignored."); + } + // When custom CNAME is used, the default-domain URL still needs the pod prefix + // so MPConnectionTest matching and pinning fallbacks continue to work. + String defaultDomain = endpoint == Endpoint.CONFIG ? defaultUrl : getPodUrl(defaultUrl, null, false); + return new ResolvedHost(customBaseURL, defaultDomain, false); + } + + String domainMappingUrl = domainMapping != null ? domainMapping.getUrl() : null; + boolean isDefaultDomain = MPUtility.isEmpty(domainMappingUrl) || defaultUrl.equals(domainMappingUrl); + String url = isDefaultDomain ? defaultUrl : domainMappingUrl; + String defaultDomain = defaultUrl; + + if (endpoint != Endpoint.CONFIG) { + if (isDefaultDomain) { + // Default domain gets the pod prefix. + url = getPodUrl(url, mConfigManager.getPodPrefix(apiKey), mConfigManager.isDirectUrlRoutingEnabled()); + } else { + // Domain-mapped: always generate the default with the original (un-pod-prefixed) host. + defaultDomain = getPodUrl(defaultDomain, null, false); + } + } + return new ResolvedHost(url, defaultDomain, isDefaultDomain); + } + + private static final class ResolvedHost { + final String url; + final String defaultDomain; + final boolean isDefaultDomain; + + ResolvedHost(String url, String defaultDomain, boolean isDefaultDomain) { + this.url = url; + this.defaultDomain = defaultDomain; + this.isDefaultDomain = isDefaultDomain; + } + } + public enum Endpoint { CONFIG(1), IDENTITY(2), diff --git a/android-core/src/main/java/com/mparticle/networking/NetworkOptions.java b/android-core/src/main/java/com/mparticle/networking/NetworkOptions.java index 6faf9d521..708aae363 100644 --- a/android-core/src/main/java/com/mparticle/networking/NetworkOptions.java +++ b/android-core/src/main/java/com/mparticle/networking/NetworkOptions.java @@ -16,11 +16,13 @@ import org.json.JSONException; import org.json.JSONObject; +import java.net.MalformedURLException; +import java.net.URL; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.HashSet; import java.util.Set; public class NetworkOptions { @@ -28,6 +30,7 @@ public class NetworkOptions { Map domainMappings = new HashMap(); boolean pinningDisabledInDevelopment = false; boolean pinningDisabled = false; + private String customBaseURL = null; private static Set loggedDomainTypes = new HashSet<>(); private NetworkOptions() { @@ -44,6 +47,10 @@ private NetworkOptions(Builder builder) { if (builder.pinningDisabled != null) { pinningDisabled = builder.pinningDisabled; } + + if (builder.customBaseURL != null) { + customBaseURL = builder.customBaseURL; + } } @NonNull @@ -61,6 +68,13 @@ public static NetworkOptions withNetworkOptions(@Nullable String jsonString) { JSONObject jsonObject = new JSONObject(jsonString); builder.setPinningDisabledInDevelopment(jsonObject.optBoolean("disableDevPinning", false)); builder.setPinningDisabled(jsonObject.optBoolean("disablePinning", false)); + String storedCustomBaseURL = jsonObject.optString("customBaseURL", null); + if (!MPUtility.isEmpty(storedCustomBaseURL)) { + // Stored value is already host(:port). Skip the Builder setter (which expects a full + // HTTPS URL with scheme) and assign directly so a previously-validated value survives + // the JSON round-trip. + builder.customBaseURL = storedCustomBaseURL; + } JSONArray domainMappingsJson = jsonObject.getJSONArray("domainMappings"); for (int i = 0; i < domainMappingsJson.length(); i++) { builder.addDomainMapping(DomainMapping @@ -106,6 +120,16 @@ public boolean isPinningDisabled() { return pinningDisabled; } + /** + * Returns the configured custom CNAME host (without scheme), or {@code null} + * if not set. When non-null, this host overrides individual domain mappings + * for all endpoints. + */ + @Nullable + public String getCustomBaseURL() { + return customBaseURL; + } + DomainMapping getDomain(Endpoint endpoint) { return domainMappings.get(endpoint); } @@ -123,6 +147,9 @@ public JSONObject toJson() { JSONArray domainMappingsJson = new JSONArray(); networkOptions.put("disableDevPinning", pinningDisabledInDevelopment); networkOptions.put("disablePinning", pinningDisabled); + if (!MPUtility.isEmpty(customBaseURL)) { + networkOptions.put("customBaseURL", customBaseURL); + } networkOptions.put("domainMappings", domainMappingsJson); for (DomainMapping domainMapping : domainMappings.values()) { domainMappingsJson.put(domainMapping.toString()); @@ -137,6 +164,7 @@ public static class Builder { private Map domainMappings = new HashMap(); private Boolean pinningDisabledInDevelopment; private Boolean pinningDisabled; + private String customBaseURL; private Builder() { } @@ -187,6 +215,40 @@ public Builder setPinningDisabled(boolean disabled) { return this; } + /** + * Routes all mParticle endpoint traffic (config, events, identity, alias, audience) + * through a single CNAME host. Must be an HTTPS URL (e.g. https://rkt.example.com). + * Non-HTTPS values are rejected with a warning log and the property is left unset. + * + *

When set, this property takes priority over any per-endpoint domain mapping. + * Any path, query, or fragment on the URL is ignored — only the scheme, host, and + * port are used. + * + *

Certificate pinning: if pinning is enabled (default), supply certificates for + * the CNAME domain via the relevant {@link DomainMapping}, or disable pinning via + * {@link #setPinningDisabled(boolean)} / {@link #setPinningDisabledInDevelopment(boolean)}. + * + * @param customBaseURL HTTPS URL containing the CNAME host + */ + @NonNull + public Builder setCustomBaseURL(@NonNull String customBaseURL) { + try { + URL parsed = new URL(customBaseURL); + if (!"https".equalsIgnoreCase(parsed.getProtocol()) || MPUtility.isEmpty(parsed.getHost())) { + Logger.warning("NetworkOptions: customBaseURL must use HTTPS and include a valid host — value ignored."); + return this; + } + StringBuilder host = new StringBuilder(parsed.getHost()); + if (parsed.getPort() != -1) { + host.append(":").append(parsed.getPort()); + } + this.customBaseURL = host.toString(); + } catch (MalformedURLException e) { + Logger.warning("NetworkOptions: customBaseURL is malformed — value ignored."); + } + return this; + } + @NonNull public NetworkOptions build() { return new NetworkOptions(this); diff --git a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKit.kt b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKit.kt index 091ba6356..81d55668f 100644 --- a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKit.kt +++ b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKit.kt @@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.lang.ref.WeakReference import java.math.BigDecimal +import java.net.URL const val ROKT_ATTRIBUTE_SANDBOX_MODE: String = "sandbox" @@ -90,6 +91,8 @@ class RoktKit : val mappedLogLevel = Logger.getMinLogLevel().toRoktLogLevel() Rokt.setLogLevel(mappedLogLevel) + applyCustomBaseURLIfSet() + Rokt.init( roktTagId = roktTagId, appVersion = info.versionName, @@ -176,6 +179,22 @@ class RoktKit : private fun throwOnKitCreateError(message: String): Unit = throw IllegalArgumentException(message) + /** + * Reads `customBaseURL` from the mParticle network options and forwards it to the + * Rokt SDK as a CNAME override. No-op if MParticle is uninitialized, the host is + * absent, or it's an empty string. + */ + private fun applyCustomBaseURLIfSet() { + val customBaseURL = MParticle.getInstance() + ?.Internal() + ?.configManager + ?.networkOptions + ?.customBaseURL + if (!customBaseURL.isNullOrEmpty()) { + Rokt.setCustomBaseURL(URL("https://$customBaseURL")) + } + } + /* For more details, visit the official documentation: https://docs.rokt.com/developers/integration-guides/android/how-to/adding-a-placement/ diff --git a/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt b/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt index 9717978a6..9c08b95a1 100644 --- a/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt +++ b/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt @@ -25,9 +25,11 @@ import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject +import io.mockk.mockkStatic import io.mockk.runs import io.mockk.slot import io.mockk.unmockkObject +import io.mockk.unmockkStatic import io.mockk.verify import io.mockk.verifyOrder import kotlinx.coroutines.flow.first @@ -1503,4 +1505,104 @@ class RoktKitTests { method.isAccessible = true assertEquals(RoktLogLevel.NONE, method.invoke(roktKit, MParticle.LogLevel.NONE)) } + + // MARK: - applyCustomBaseURLIfSet + + private fun invokeApplyCustomBaseURLIfSet() { + val method = RoktKit::class.java.getDeclaredMethod("applyCustomBaseURLIfSet") + method.isAccessible = true + method.invoke(roktKit) + } + + private fun stubNetworkOptionsCustomBaseURL(customBaseURL: String?): com.mparticle.networking.NetworkOptions { + val mockInternal = mock(MParticle.Internal::class.java) + val mockConfigManager = mock(com.mparticle.internal.ConfigManager::class.java) + val mockNetworkOptions = mock(com.mparticle.networking.NetworkOptions::class.java) + Mockito.`when`(MParticle.getInstance()?.Internal()).thenReturn(mockInternal) + Mockito.`when`(mockInternal.configManager).thenReturn(mockConfigManager) + Mockito.`when`(mockConfigManager.networkOptions).thenReturn(mockNetworkOptions) + Mockito.`when`(mockNetworkOptions.customBaseURL).thenReturn(customBaseURL) + return mockNetworkOptions + } + + @Test + fun applyCustomBaseURLIfSet_whenHostIsSet_forwardsHttpsURLToRokt() { + stubNetworkOptionsCustomBaseURL("rkt.example.com") + mockkStatic(Rokt::class) + every { Rokt.setCustomBaseURL(any()) } just runs + + invokeApplyCustomBaseURLIfSet() + + verify(exactly = 1) { + Rokt.setCustomBaseURL(java.net.URL("https://rkt.example.com")) + } + unmockkStatic(Rokt::class) + } + + @Test + fun applyCustomBaseURLIfSet_whenHostHasPort_preservesPortInURL() { + stubNetworkOptionsCustomBaseURL("rkt.example.com:8443") + mockkStatic(Rokt::class) + every { Rokt.setCustomBaseURL(any()) } just runs + + invokeApplyCustomBaseURLIfSet() + + verify(exactly = 1) { + Rokt.setCustomBaseURL(java.net.URL("https://rkt.example.com:8443")) + } + unmockkStatic(Rokt::class) + } + + @Test + fun applyCustomBaseURLIfSet_whenHostIsNull_doesNotCallRokt() { + stubNetworkOptionsCustomBaseURL(null) + mockkStatic(Rokt::class) + every { Rokt.setCustomBaseURL(any()) } just runs + + invokeApplyCustomBaseURLIfSet() + + verify(exactly = 0) { Rokt.setCustomBaseURL(any()) } + unmockkStatic(Rokt::class) + } + + @Test + fun applyCustomBaseURLIfSet_whenHostIsEmpty_doesNotCallRokt() { + stubNetworkOptionsCustomBaseURL("") + mockkStatic(Rokt::class) + every { Rokt.setCustomBaseURL(any()) } just runs + + invokeApplyCustomBaseURLIfSet() + + verify(exactly = 0) { Rokt.setCustomBaseURL(any()) } + unmockkStatic(Rokt::class) + } + + @Test + fun applyCustomBaseURLIfSet_whenNetworkOptionsIsNull_doesNotCallRokt() { + val mockInternal = mock(MParticle.Internal::class.java) + val mockConfigManager = mock(com.mparticle.internal.ConfigManager::class.java) + Mockito.`when`(MParticle.getInstance()?.Internal()).thenReturn(mockInternal) + Mockito.`when`(mockInternal.configManager).thenReturn(mockConfigManager) + Mockito.`when`(mockConfigManager.networkOptions).thenReturn(null) + + mockkStatic(Rokt::class) + every { Rokt.setCustomBaseURL(any()) } just runs + + invokeApplyCustomBaseURLIfSet() + + verify(exactly = 0) { Rokt.setCustomBaseURL(any()) } + unmockkStatic(Rokt::class) + } + + @Test + fun applyCustomBaseURLIfSet_whenMParticleInstanceIsNull_doesNotCallRokt() { + MParticle.setInstance(null) + mockkStatic(Rokt::class) + every { Rokt.setCustomBaseURL(any()) } just runs + + invokeApplyCustomBaseURLIfSet() + + verify(exactly = 0) { Rokt.setCustomBaseURL(any()) } + unmockkStatic(Rokt::class) + } }