Feedback interface to use feedback widgets nps,rating and forms.
* And do manual reporting of feedbacks.
*
- * @return {@link ModuleFeedback.Feedback} instance.
+ * @return {@link ModuleFeedback.Feedback} instance, never {@code null}: before init, or while the feature does not
+ * run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleFeedback.Feedback feedback() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] SDK is not initialized yet.");
}
- return null;
+ return SDKCore.dormantFeedback;
}
return sdk.feedback();
}
@@ -413,7 +414,8 @@ public ModuleFeedback.Feedback feedback() {
* content block. A {@link ly.count.sdk.java.internal.ContentDisplay} has to be registered
* before a content zone can be entered; the "ly.count.sdk:java-ui" artifact ships a JavaFX one.
*
- * @return {@link ModuleContent.Content} instance.
+ * @return {@link ModuleContent.Content} instance, never {@code null}: before init, or while the feature does not
+ * run, as its consent is not given or it is not enabled, one that ignores calls until it runs
* @apiNote This is an EXPERIMENTAL feature, and it can have breaking changes
*/
public ModuleContent.Content content() {
@@ -421,7 +423,7 @@ public ModuleContent.Content content() {
if (L != null) {
L.e("[Countly] content, SDK is not initialized yet.");
}
- return null;
+ return SDKCore.dormantContent;
}
return sdk.content();
}
@@ -429,14 +431,15 @@ public ModuleContent.Content content() {
/**
* RemoteConfig interface to use remote config feature.
*
- * @return {@link ModuleRemoteConfig.RemoteConfig} instance.
+ * @return {@link ModuleRemoteConfig.RemoteConfig} instance, never {@code null}: before init, or while the feature does not
+ * run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleRemoteConfig.RemoteConfig remoteConfig() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] remoteConfig, SDK is not initialized yet.");
}
- return null;
+ return SDKCore.dormantRemoteConfig;
}
return sdk.remoteConfig();
}
@@ -444,14 +447,15 @@ public ModuleRemoteConfig.RemoteConfig remoteConfig() {
/**
* DeviceId interface to use device id functionalities.
*
- * @return {@link ModuleDeviceIdCore.DeviceId} instance.
+ * @return {@link ModuleDeviceIdCore.DeviceId} instance, never {@code null}: before init, or while the feature does not
+ * run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleDeviceIdCore.DeviceId deviceId() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] SDK is not initialized yet.");
}
- return null;
+ return SDKCore.dormantDeviceId;
}
return sdk.deviceId();
}
@@ -472,14 +476,15 @@ public Event event(String key) {
/**
* Event module calls
*
- * @return event module otherwise null if SDK is not initialized
+ * @return {@link ModuleEvents.Events} instance, never {@code null}: before init, or while the feature does not
+ * run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleEvents.Events events() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] SDK is not initialized yet.");
}
- return null;
+ return SDKCore.dormantEvents;
}
return sdk.events();
}
@@ -487,14 +492,15 @@ public ModuleEvents.Events events() {
/**
* Crash module calls
*
- * @return crash module otherwise null if SDK is not initialized
+ * @return {@link ModuleCrashes.Crashes} instance, never {@code null}: before init, or while the feature does not
+ * run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleCrashes.Crashes crashes() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] SDK is not initialized yet.");
}
- return null;
+ return SDKCore.dormantCrashes;
}
return sdk.crashes();
}
@@ -502,14 +508,15 @@ public ModuleCrashes.Crashes crashes() {
/**
* Views interface to use views feature.
*
- * @return {@link ModuleViews.Views} instance.
+ * @return {@link ModuleViews.Views} instance, never {@code null}: before init, or while the feature does not
+ * run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleViews.Views views() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] SDK is not initialized yet.");
}
- return null;
+ return SDKCore.dormantViews;
}
return sdk.views();
}
@@ -530,14 +537,15 @@ public Event timedEvent(String key) {
/**
* UserProfile interface to use user profile feature.
*
- * @return {@link ModuleUserProfile.UserProfile} instance.
+ * @return {@link ModuleUserProfile.UserProfile} instance, never {@code null}: before init, or while the feature does not
+ * run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleUserProfile.UserProfile userProfile() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] userProfile, SDK is not initialized yet.");
}
- return null;
+ return SDKCore.dormantUserProfile;
}
return sdk.userProfile();
}
@@ -545,7 +553,8 @@ public ModuleUserProfile.UserProfile userProfile() {
/**
* Location interface to use location feature.
*
- * @return {@link ModuleLocation.Location} instance.
+ * @return {@link ModuleLocation.Location} instance, never {@code null}: before init, or while the feature does not
+ * run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleLocation.Location location() {
if (!isInitialized()) {
@@ -553,7 +562,7 @@ public ModuleLocation.Location location() {
if (L != null) {
L.e("Countly.sharedInstance().init must be called before accessing location");
}
- return null;
+ return SDKCore.dormantLocation;
}
return sdk.location();
@@ -574,25 +583,25 @@ public User user() {
@Override
public Usage addParam(String key, Object value) {
- L.d("[Countly] addParam: key = " + key + " value = " + value);
+ L.d("[Countly] addParam: key = " + key);
return ((Session) sdk.session(null)).addParam(key, value);
}
@Override
public Usage addCrashReport(Throwable t, boolean fatal) {
- L.d("[Countly] addCrashReport: t = " + t + " fatal = " + fatal);
+ L.d("[Countly] addCrashReport: fatal = " + fatal);
return ((Session) sdk.session(null)).addCrashReport(t, fatal);
}
@Override
public Usage addCrashReport(Throwable t, boolean fatal, String name, Map* They dereferenced a module that was never built, and consent is not the gate that stops them: * with consent not required, the consent check passes and the null module was used anyway. */ @Test - public void accessors_forFeaturesThatWereNeverEnabled_returnNull() { + public void accessors_forFeaturesThatWereNeverEnabled_handOutInterfacesThatIgnoreCalls() { Countly.instance().init(TestUtils.getConfigEvents(2)); - Assert.assertNull(Countly.instance().feedback()); - Assert.assertNull(Countly.instance().crashes()); - Assert.assertNull(Countly.instance().remoteConfig()); - Assert.assertNull(Countly.instance().content()); + Assert.assertSame(SDKCore.dormantFeedback, Countly.instance().feedback()); + Assert.assertSame(SDKCore.dormantCrashes, Countly.instance().crashes()); + Assert.assertSame(SDKCore.dormantRemoteConfig, Countly.instance().remoteConfig()); + Assert.assertSame(SDKCore.dormantContent, Countly.instance().content()); + int queued = TestUtils.getCurrentRQ().length; + Countly.instance().crashes().recordHandledException(new RuntimeException("never enabled")); + Countly.instance().content().enterContentZone(); + Assert.assertEquals(queued, TestUtils.getCurrentRQ().length); // The feature that IS enabled still works, so the guards did not break the happy path. - Assert.assertNotNull(Countly.instance().events()); + Assert.assertNotSame(SDKCore.dormantEvents, Countly.instance().events()); } /** - * Accessors used before init log and return null. "location" logged through a logger that does - * not exist yet, unlike every sibling accessor. + * Accessors used before init log and hand out an interface that ignores calls, backend mode + * aside, which still answers null. "location" logged through a logger that does not exist yet, + * unlike every sibling accessor. */ @Test - public void accessors_beforeInit_returnNullWithoutCrashing() { + public void accessors_beforeInit_doNotThrow() { Countly.instance().halt(); - Assert.assertNull(Countly.instance().location()); - Assert.assertNull(Countly.instance().feedback()); - Assert.assertNull(Countly.instance().content()); - Assert.assertNull(Countly.instance().remoteConfig()); + Assert.assertSame(SDKCore.dormantLocation, Countly.instance().location()); + Assert.assertSame(SDKCore.dormantFeedback, Countly.instance().feedback()); + Assert.assertSame(SDKCore.dormantContent, Countly.instance().content()); + Assert.assertSame(SDKCore.dormantRemoteConfig, Countly.instance().remoteConfig()); Assert.assertNull(Countly.instance().backendM()); } diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/CountlyFacadeTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/CountlyFacadeTests.java index 57593b82..a36435d2 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/CountlyFacadeTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/CountlyFacadeTests.java @@ -1,5 +1,8 @@ package ly.count.sdk.java.internal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -21,9 +24,9 @@ /** * The static {@link Countly} facade and the {@link SDKCore} accessors behind it. *
- * Two contracts are covered: before {@code init} every accessor must answer with null and a logged
- * error instead of throwing, and after {@code init} the deprecated {@link Usage} API must still route
- * to the live session. Both matter because they are the surface an integration touches first.
+ * Two contracts are covered: before {@code init} no accessor may throw, and every feature interface
+ * is handed out but ignores calls, and after {@code init} the deprecated {@link Usage} API must still
+ * route to the live session. Both matter because they are the surface an integration touches first.
*/
@RunWith(JUnit4.class)
public class CountlyFacadeTests {
@@ -42,26 +45,41 @@ public void stop() {
/**
* Nothing may throw before {@code init}. Every module accessor and every state query is walked in
- * one pass, so a newly added accessor that forgets its guard shows up here.
+ * one pass, so a newly added accessor that forgets its guard shows up here. Each feature interface
+ * is handed out and ignores calls, so code that calls it before init does not throw either, and a
+ * call with a callback gets its answer.
*/
@Test
- public void beforeInit_everyAccessorAnswersNullInsteadOfThrowing() {
+ public void beforeInit_noAccessorThrows_andEveryFeatureInterfaceIgnoresCalls() {
Assert.assertFalse(Countly.isInitialized());
Assert.assertNull(Countly.session());
Assert.assertNull(Countly.instance().backendM());
Assert.assertNull(Countly.backendMode());
- Assert.assertNull(Countly.instance().feedback());
- Assert.assertNull(Countly.instance().content());
- Assert.assertNull(Countly.instance().events());
- Assert.assertNull(Countly.instance().views());
- Assert.assertNull(Countly.instance().crashes());
- Assert.assertNull(Countly.instance().remoteConfig());
- Assert.assertNull(Countly.instance().deviceId());
- Assert.assertNull(Countly.instance().userProfile());
- // Countly.location() is deliberately NOT called here: its own uninitialised guard logs
- // through the instance logger without a null check, so it throws NPE before it can return
- // null. Reported as a bug rather than asserted, so a fix does not have to touch this test.
+ Assert.assertSame(SDKCore.dormantFeedback, Countly.instance().feedback());
+ Assert.assertSame(SDKCore.dormantContent, Countly.instance().content());
+ Assert.assertSame(SDKCore.dormantEvents, Countly.instance().events());
+ Assert.assertSame(SDKCore.dormantViews, Countly.instance().views());
+ Assert.assertSame(SDKCore.dormantCrashes, Countly.instance().crashes());
+ Assert.assertSame(SDKCore.dormantRemoteConfig, Countly.instance().remoteConfig());
+ Assert.assertSame(SDKCore.dormantDeviceId, Countly.instance().deviceId());
+ Assert.assertSame(SDKCore.dormantUserProfile, Countly.instance().userProfile());
+ Assert.assertSame(SDKCore.dormantLocation, Countly.instance().location());
+
+ Assert.assertFalse(Countly.instance().events().startEvent("before_init"));
+ Countly.instance().events().recordEvent("before_init");
+ Assert.assertNull(Countly.instance().views().startView("home"));
+ Countly.instance().crashes().recordHandledException(new RuntimeException("before init"));
+ Assert.assertNull(Countly.instance().deviceId().getID());
+ Assert.assertTrue(Countly.instance().remoteConfig().getValues().isEmpty());
+ Countly.instance().userProfile().setProperty("plan", "pro");
+ Countly.instance().location().setLocation("TR", "Izmir", "38.4,27.1", null);
+ Countly.instance().content().enterContentZone();
+ List
+ * Verifies that an events interface handed out before init and one handed out before the consent
+ * record once the consent is given, that a views interface handed out before the consent starts a
+ * view then, and that a running events interface kept across a removal of its consent ignores
+ * calls until the consent is given again and then records into the new module.
+ */
+ @Test
+ public void aKeptInterface_reachesTheModuleOnceItsFeatureRuns() {
+ ModuleEvents.Events beforeInit = Countly.instance().events();
+ Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Events, Config.Feature.Views)
+ .setRequiresConsent(true).setEventQueueSizeToSend(100));
+ ModuleEvents.Events beforeConsent = Countly.instance().events();
+ ModuleViews.Views viewsBeforeConsent = Countly.instance().views();
+ beforeConsent.recordEvent("ignored_before_consent");
+ Assert.assertNull(viewsBeforeConsent.startView("ignored"));
+ TestUtils.validateEQSize(0);
+
+ Countly.onConsent(Config.Feature.Events, Config.Feature.Views);
+
+ beforeInit.recordEvent("from_before_init");
+ beforeConsent.recordEvent("from_before_consent");
+ Assert.assertNotNull(viewsBeforeConsent.startView("home"));
+ Assert.assertTrue(beforeConsent.startEvent("timed"));
+ Assert.assertTrue(Countly.instance().events().endEvent("timed"));
+ Assert.assertEquals(Arrays.asList("from_before_init", "from_before_consent", ModuleViews.KEY_VIEW_EVENT, "timed"), TestUtils.keysInEQ());
+
+ ModuleEvents.Events running = Countly.instance().events();
+ Countly.onConsentRemoval(Config.Feature.Events);
+ running.recordEvent("ignored_after_removal");
+ TestUtils.validateEQSize(0);
+
+ Countly.onConsent(Config.Feature.Events);
+ running.recordEvent("after_consent_again");
+ Assert.assertEquals(Collections.singletonList("after_consent_again"), TestUtils.keysInEQ());
+ }
+
+ /**
+ * Events carry the view IDs of the views module that runs now.
+ *
+ * Verifies that while the views consent is removed an event carries no current view ID, and that
+ * once it is given again an event carries the ID of the view the new module started, as does the
+ * next view as its previous one.
+ */
+ @Test
+ public void viewIds_followTheViewsModuleThatRuns_acrossAViewsConsentChange() {
+ Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Events, Config.Feature.Views)
+ .setRequiresConsent(true).setEventQueueSizeToSend(100));
+ Countly.onConsent(Config.Feature.Events, Config.Feature.Views);
+ Assert.assertNotNull(Countly.instance().views().startView("first"));
+
+ Countly.onConsentRemoval(Config.Feature.Views);
+ Countly.instance().events().recordEvent("without_views");
+ Countly.onConsent(Config.Feature.Views);
+ String second = Countly.instance().views().startView("second");
+ Countly.instance().events().recordEvent("with_views");
+ Countly.instance().views().startView("third");
+
+ List
+ * Verifies that after the location consent is removed the location interface ignores calls while
+ * the device ID, events, views and user profile interfaces keep working and recording, and that
+ * an events interface kept from before the events consent is removed ignores calls instead of
+ * recording into the stopped module.
+ */
+ @Test
+ public void consentRemoval_stopsOnlyTheFeaturesItNames_andAKeptInterfaceIgnoresCalls() {
+ Countly.instance().init(TestUtils.getBaseConfig()
+ .enableFeatures(Config.Feature.Events, Config.Feature.Views, Config.Feature.Location, Config.Feature.UserProfiles)
+ .setRequiresConsent(true).setEventQueueSizeToSend(100));
+ Countly.onConsent(Config.Feature.values());
+ ModuleEvents.Events keptEvents = Countly.instance().events();
+
+ Countly.onConsentRemoval(Config.Feature.Location);
+
+ Assert.assertFalse(Countly.isTracking(Config.Feature.Location));
+ Assert.assertSame(SDKCore.dormantLocation, Countly.instance().location());
+ Assert.assertEquals(TestUtils.DEVICE_ID, Countly.instance().deviceId().getID());
+ Assert.assertSame(keptEvents, Countly.instance().events());
+ keptEvents.recordEvent("after_location_removal");
+ Countly.instance().views().startView("home");
+ Assert.assertEquals(2, TestUtils.getCurrentEQ().size());
+ Countly.instance().userProfile().setProperty("plan", "pro");
+ Countly.instance().userProfile().save();
+ Assert.assertEquals(1, requestsContaining("user_details"));
+
+ Countly.onConsentRemoval(Config.Feature.Events);
+
+ Assert.assertSame(SDKCore.dormantEvents, Countly.instance().events());
+ keptEvents.recordEvent("after_events_removal");
+ Assert.assertFalse(keptEvents.startEvent("timed_after_events_removal"));
+ TestUtils.validateEQSize(0);
+ Assert.assertEquals(TestUtils.DEVICE_ID, Countly.instance().deviceId().getID());
}
/**
@@ -358,7 +547,7 @@ public void onRequestCompleted(Request request, String response, int responseCod
Assert.assertTrue("failed: " + failures, failures.isEmpty());
Assert.assertTrue(handedOver.get() > 0);
Assert.assertEquals(handedOver.get(), delivered.get());
- Assert.assertNull(Countly.instance().feedback());
+ Assert.assertSame(SDKCore.dormantFeedback, Countly.instance().feedback());
} finally {
SDKCore.testDummyModule = null;
}
diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/CrashFlowTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/CrashFlowTests.java
index 2b488c34..7a0f0cc6 100644
--- a/sdk-java/src/test/java/ly/count/sdk/java/internal/CrashFlowTests.java
+++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/CrashFlowTests.java
@@ -3,6 +3,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
import ly.count.sdk.java.Config;
import ly.count.sdk.java.Countly;
import ly.count.sdk.java.Crash;
@@ -138,8 +139,36 @@ public void crashReporting_refusesBadInputAndStandsDownUnderBackendMode() {
}
/**
- * Consent gating. With consent required and CrashReporting not granted the interface is withheld
- * entirely; once granted the crash flows.
+ * With consent required, an unhandled crash is recorded once the crash reporting consent is given
+ * after init, and still reaches the handler the application installed.
+ */
+ @Test
+ public void unhandledCrash_isRecorded_whenTheConsentIsGivenAfterInit() throws InterruptedException {
+ Thread.UncaughtExceptionHandler before = Thread.getDefaultUncaughtExceptionHandler();
+ List
- * Verifies that with a blacklist, listed keys, spaces and dashes included, are dropped with one
- * warning each, timed events too, while an unlisted key, a view and an internal event listed in
- * the blacklist are recorded; that a response bringing a whitelist replaces the blacklist, so only
+ * Verifies that with a blacklist, listed keys, spaces and dashes and a {@code [CLY]_} key the SDK
+ * does not record itself included, are dropped with one warning each, timed events too, while an
+ * unlisted key, a view and an internal event listed in the blacklist are recorded; that a response bringing a whitelist replaces the blacklist, so only
* its keys are recorded, a view still is and a key the blacklist dropped is dropped again; and that
* an empty whitelist allows every key.
*/
@Test
public void eventFilter_blacklistThenWhitelist_decideCustomEventsOnly_andAnEmptyListAllowsEverything() {
server.respondWith(new ServerConfigBuilder()
- .eventFilterList(names("blocked_event", "event with spaces", "event-with-dashes", ModuleViews.KEY_VIEW_EVENT, "[CLY]_star_rating"), false)
+ .eventFilterList(names("blocked_event", "event with spaces", "event-with-dashes", "[CLY]_not_an_sdk_event", ModuleViews.KEY_VIEW_EVENT, "[CLY]_star_rating"), false)
.networking(false));
init(configWithWarnings().enableFeatures(Config.Feature.Events, Config.Feature.Views).setEventQueueSizeToSend(100));
Countly.instance().events().recordEvent("blocked_event");
Countly.instance().events().recordEvent("event with spaces");
Countly.instance().events().recordEvent("event-with-dashes", TestUtils.map("colour", "red"));
+ Countly.instance().events().recordEvent("[CLY]_not_an_sdk_event");
Countly.instance().events().startEvent("blocked_event");
Countly.instance().events().endEvent("blocked_event");
TestUtils.validateEQSize(0);
- Assert.assertEquals(4, warningsContaining("is filtered out by the event filter"));
+ Assert.assertEquals(5, warningsContaining("is filtered out by the event filter"));
Countly.instance().events().recordEvent("allowed_event");
Countly.instance().views().startView("home");
Countly.instance().events().recordEvent("[CLY]_star_rating");
- Assert.assertEquals(Arrays.asList("allowed_event", ModuleViews.KEY_VIEW_EVENT, "[CLY]_star_rating"), keysInEQ());
+ Assert.assertEquals(Arrays.asList("allowed_event", ModuleViews.KEY_VIEW_EVENT, "[CLY]_star_rating"), TestUtils.keysInEQ());
push(new ServerConfigBuilder().eventFilterList(names("allowed_event", "another_allowed"), true));
Countly.instance().events().recordEvent("allowed_event");
@@ -96,15 +97,15 @@ public void eventFilter_blacklistThenWhitelist_decideCustomEventsOnly_andAnEmpty
Countly.instance().events().recordEvent("blocked_event");
Countly.instance().views().stopViewWithName("home");
Assert.assertEquals(Arrays.asList("allowed_event", ModuleViews.KEY_VIEW_EVENT, "[CLY]_star_rating",
- "allowed_event", "another_allowed", ModuleViews.KEY_VIEW_EVENT), keysInEQ());
- Assert.assertEquals(6, warningsContaining("is filtered out by the event filter"));
+ "allowed_event", "another_allowed", ModuleViews.KEY_VIEW_EVENT), TestUtils.keysInEQ());
+ Assert.assertEquals(7, warningsContaining("is filtered out by the event filter"));
push(new ServerConfigBuilder().eventFilterList(names(), true));
Countly.instance().events().recordEvent("not_in_whitelist");
Countly.instance().events().recordEvent("blocked_event");
- List
* Verifies that a {@code cr} arriving while the SDK runs is stored but leaves the running SDK
* without consent gating, that the stored value is in place before the modules are built on the
- * next init, so the feature modules wait for consent, and that removing consent stops feature
- * modules but never the configuration module, which keeps applying responses.
+ * next init, so the feature modules wait for consent while an integration that never asked for
+ * consent calls an events interface that ignores calls instead of throwing, and that removing
+ * consent stops feature modules but never the configuration module, which keeps applying
+ * responses.
*/
@Test
public void consentRequirement_appliesAtInitOnly_andConsentRemovalNeverStopsTheConfigurationModule() {
@@ -399,7 +401,9 @@ public void consentRequirement_appliesAtInitOnly_andConsentRemovalNeverStopsTheC
Assert.assertTrue(SDKCore.instance.config.requiresConsent());
Assert.assertFalse(Countly.isTracking(Config.Feature.Events));
- Assert.assertNull(Countly.instance().events());
+ Assert.assertSame(SDKCore.dormantEvents, Countly.instance().events());
+ Countly.instance().events().recordEvent("before_consent");
+ TestUtils.validateEQSize(0);
Countly.onConsent(Config.Feature.Events);
Assert.assertTrue(Countly.isTracking(Config.Feature.Events));
@@ -479,7 +483,8 @@ public void timers_followTheResolvedIntervals_andChangesAreDispatchedOnce() thro
/**
* {@code cet} covers custom events only, {@code tracking} covers every event.
*
- * Verifies on the wire that with custom event tracking off a custom event is dropped while a view
+ * Verifies on the wire that with custom event tracking off a custom event, a {@code [CLY]_} key
+ * the SDK does not record itself included, is dropped while a view
* started through the views interface and an internal event still reach the queue, flushed at the
* event queue size the settings resolved, that tracking off then drops internal events too, and
* that custom events are recorded again once both are back on. Networking is off throughout, so
@@ -491,6 +496,7 @@ public void customEventTracking_off_dropsCustomEventsOnly_whileTrackingGatesEver
init(TestUtils.getConfigSdkBehaviorSettings().enableFeatures(Config.Feature.Events, Config.Feature.Views).setEventQueueSizeToSend(100));
Countly.instance().events().recordEvent("custom_event");
+ Countly.instance().events().recordEvent("[CLY]_not_an_sdk_event");
TestUtils.validateEQSize(0);
Countly.instance().views().startView("home");
diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleContentTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleContentTests.java
index 1691226a..81639399 100644
--- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleContentTests.java
+++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleContentTests.java
@@ -129,8 +129,9 @@ public void enterContentZone_withoutADisplay_isIgnored() throws JSONException {
}
/**
- * With consent required, the content interface is unreachable until content consent is given,
- * and a zone that is already running is torn down when that consent is taken away again.
+ * With consent required, the content interface ignores calls until content consent is given, and
+ * a zone that is already running is torn down when that consent is taken away again, after which
+ * the interface kept from before ignores calls too.
*/
@Test
public void content_isGatedByConsent() throws JSONException {
@@ -138,7 +139,7 @@ public void content_isGatedByConsent() throws JSONException {
init(config);
nextResponse = new JSONObject(CONTENT_RESPONSE);
- Assert.assertNull(Countly.instance().content());
+ Assert.assertSame(SDKCore.dormantContent, Countly.instance().content());
Countly.onConsent(Config.Feature.Content, Config.Feature.Events);
installRequestMaker();
@@ -151,7 +152,9 @@ public void content_isGatedByConsent() throws JSONException {
Assert.assertEquals(1, requests.size());
Countly.onConsentRemoval(Config.Feature.Content);
- Assert.assertNull(Countly.instance().content());
+ Assert.assertSame(SDKCore.dormantContent, Countly.instance().content());
+ content.enterContentZone();
+ content.refreshContentZone();
Assert.assertEquals(1, requests.size());
}
diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleLocationsTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleLocationsTests.java
index 6784b085..139c5671 100644
--- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleLocationsTests.java
+++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleLocationsTests.java
@@ -35,6 +35,21 @@ public void disableLocation() {
validateLocationRequestInRQ(TestUtils.map("location", ""), 0);
}
+ /**
+ * Removing the location consent erases the location on the server right away
+ * Request queue should hold the location request and then one with an empty location
+ */
+ @Test
+ public void locationConsentRemoval_erasesTheLocation() {
+ Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Location).setRequiresConsent(true));
+ Countly.onConsent(Config.Feature.Location);
+ Countly.instance().location().setLocation("TR", "Izmir", "38.4,27.1", null);
+
+ Countly.onConsentRemoval(Config.Feature.Location);
+
+ validateLocationRequestInRQ(TestUtils.map("location", ""), 1);
+ }
+
/**
* "disableLocation"
* Validating that empty location parameter is not sent because location is not enabled
@@ -43,7 +58,8 @@ public void disableLocation() {
@Test
public void disableLocation_noConsent() {
Countly.instance().init(TestUtils.getBaseConfig());
- Assert.assertNull(Countly.instance().location());
+ Assert.assertSame(SDKCore.dormantLocation, Countly.instance().location());
+ Countly.instance().location().disableLocation();
Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
}
diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleRemoteConfigTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleRemoteConfigTests.java
index 346a8038..0f24654d 100644
--- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleRemoteConfigTests.java
+++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleRemoteConfigTests.java
@@ -1,7 +1,11 @@
package ly.count.sdk.java.internal;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
import java.util.Map;
+import ly.count.sdk.java.Config;
import ly.count.sdk.java.Countly;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -134,11 +138,10 @@ public void getValue_nullKey() {
@Test
public void registerDownloadCallback_null() {
Countly.instance().init(TestUtils.getConfigRemoteConfigs());
- ModuleRemoteConfig rcModule = SDKCore.instance.module(ModuleRemoteConfig.class);
- Assert.assertEquals(0, rcModule.downloadCallbacks.size());
+ Assert.assertEquals(0, SDKCore.instance.remoteConfigDownloadCallbacks.size());
Countly.instance().remoteConfig().registerDownloadCallback(null);
- Assert.assertEquals(0, rcModule.downloadCallbacks.size());
+ Assert.assertEquals(0, SDKCore.instance.remoteConfigDownloadCallbacks.size());
}
/**
@@ -149,12 +152,32 @@ public void registerDownloadCallback_null() {
@Test
public void registerDownloadCallback() {
Countly.instance().init(TestUtils.getConfigRemoteConfigs());
- ModuleRemoteConfig rcModule = SDKCore.instance.module(ModuleRemoteConfig.class);
- Assert.assertEquals(0, rcModule.downloadCallbacks.size());
+ Assert.assertEquals(0, SDKCore.instance.remoteConfigDownloadCallbacks.size());
Countly.instance().remoteConfig().registerDownloadCallback((rResult, error, fullValueUpdate, downloadedValues) -> {
});
- Assert.assertEquals(1, rcModule.downloadCallbacks.size());
+ Assert.assertEquals(1, SDKCore.instance.remoteConfigDownloadCallbacks.size());
+ }
+
+ /**
+ * "registerDownloadCallback" before the consent
+ * Validating that a callback registered while remote config does not run is called by the module
+ * started once the consent is given, and that one removed meanwhile is not
+ */
+ @Test
+ public void registerDownloadCallback_beforeTheConsent_isCalledOnceRemoteConfigRuns() {
+ Countly.instance().init(TestUtils.getConfigRemoteConfigs().setRequiresConsent(true));
+ List