diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt index 8a24454a6..46968b3e7 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt @@ -146,7 +146,7 @@ internal class LoginUserOperationExecutor( var identities = mapOf() var subscriptions = mapOf() val properties = mutableMapOf() - properties["timezone_id"] = TimeUtils.getTimeZoneId()!! + properties["timezone_id"] = TimeUtils.getTimeZoneId() properties["language"] = _languageContext.language if (createUserOperation.externalId != null) { diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/TimeUtilsTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/TimeUtilsTest.kt new file mode 100644 index 000000000..b47072737 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/TimeUtilsTest.kt @@ -0,0 +1,48 @@ +package com.onesignal.common + +import android.os.Build +import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldNotBeEmpty +import io.kotest.matchers.string.shouldNotContain +import java.time.ZoneId +import java.util.TimeZone + +@RobolectricTest +class TimeUtilsTest : FunSpec({ + + test("getTimeZoneId returns correct time zone id") { + // Given + val expected = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ZoneId.systemDefault().id + } else { + TimeZone.getDefault().id + } + + // When + val actual = TimeUtils.getTimeZoneId() + + // Then + actual shouldBe expected + actual.shouldNotBeEmpty() + } + + test("getTimeZoneId returns valid timezone format") { + // When + val timeZoneId = TimeUtils.getTimeZoneId() + + // Then + timeZoneId.shouldNotBeEmpty() + timeZoneId shouldNotBe "" + + // Valid timezone IDs follow IANA format patterns: + // - Continental zones: "America/New_York", "Europe/London" + // - UTC variants: "UTC", "GMT" + // - Offset formats: "GMT+05:30", "UTC-08:00" + // Should not contain spaces or invalid characters + timeZoneId.shouldNotContain(" ") + } +}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/session/impl/SessionListenerTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/session/impl/SessionListenerTests.kt new file mode 100644 index 000000000..de81391f6 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/session/impl/SessionListenerTests.kt @@ -0,0 +1,57 @@ +package com.onesignal.session.internal.session.impl + +import com.onesignal.common.TimeUtils +import com.onesignal.mocks.MockHelper +import com.onesignal.user.internal.operations.TrackSessionStartOperation +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import io.mockk.verify + +class SessionListenerTests : FunSpec({ + + test("onSessionStarted updates timezone and enqueues TrackSessionStartOperation") { + // Given + val mockTimeZone = "Europe/London" + mockkObject(TimeUtils) + every { TimeUtils.getTimeZoneId() } returns mockTimeZone + + val mockOperationRepo = mockk(relaxed = true) + val mockSessionService = mockk(relaxed = true) + val mockConfigModelStore = MockHelper.configModelStore() + val mockIdentityModelStore = MockHelper.identityModelStore() + val mockOutcomeEventsController = mockk(relaxed = true) + + val mockPropertiesModelStore = MockHelper.propertiesModelStore() + + val sessionListener = + SessionListener( + mockOperationRepo, + mockSessionService, + mockConfigModelStore, + mockIdentityModelStore, + mockPropertiesModelStore, + mockOutcomeEventsController, + ) + + try { + // When + sessionListener.onSessionStarted() + + // Then - Verify that update() was called (timezone should be set to our mocked value) + val propertiesModel = mockPropertiesModelStore.model + propertiesModel.timezone shouldBe mockTimeZone + + // Also verify the operation was enqueued + verify(exactly = 1) { + mockOperationRepo.enqueue(any(), true) + } + } finally { + // Clean up the mock + unmockkObject(TimeUtils) + } + } +}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt index 07acdd60c..c37565434 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt @@ -1,5 +1,6 @@ package com.onesignal.user.internal.operations +import com.onesignal.common.TimeUtils import com.onesignal.common.exceptions.BackendException import com.onesignal.common.modeling.ModelChangeTags import com.onesignal.core.internal.operations.ExecutionResult @@ -27,7 +28,9 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.just import io.mockk.mockk +import io.mockk.mockkObject import io.mockk.runs +import io.mockk.unmockkObject class RefreshUserOperationExecutorTests : FunSpec({ val appId = "appId" @@ -327,4 +330,71 @@ class RefreshUserOperationExecutorTests : FunSpec({ mockUserBackendService.getUser(appId, IdentityConstants.ONESIGNAL_ID, remoteOneSignalId) } } + + test("refresh user sets local timezone via propertiesModel update") { + // Given + val mockTimeZone = "America/New_York" + mockkObject(TimeUtils) + every { TimeUtils.getTimeZoneId() } returns mockTimeZone + + val mockUserBackendService = mockk() + coEvery { mockUserBackendService.getUser(appId, IdentityConstants.ONESIGNAL_ID, remoteOneSignalId) } returns + CreateUserResponse( + mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), + PropertiesObject(country = "US"), + listOf(), + ) + + val mockIdentityModelStore = MockHelper.identityModelStore() + val mockIdentityModel = IdentityModel() + mockIdentityModel.onesignalId = remoteOneSignalId + every { mockIdentityModelStore.model } returns mockIdentityModel + every { mockIdentityModelStore.replace(any(), any()) } just runs + + val mockPropertiesModelStore = MockHelper.propertiesModelStore() + val mockPropertiesModel = PropertiesModel() + mockPropertiesModel.onesignalId = remoteOneSignalId + every { mockPropertiesModelStore.model } returns mockPropertiesModel + every { mockPropertiesModelStore.replace(any(), any()) } just runs + + val mockSubscriptionsModelStore = mockk() + every { mockSubscriptionsModelStore.replaceAll(any(), any()) } just runs + + val mockConfigModelStore = MockHelper.configModelStore() + val mockBuildUserService = mockk() + + val refreshUserOperationExecutor = + RefreshUserOperationExecutor( + mockUserBackendService, + mockIdentityModelStore, + mockPropertiesModelStore, + mockSubscriptionsModelStore, + mockConfigModelStore, + mockBuildUserService, + getNewRecordState(), + ) + + val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId)) + + try { + // When + val response = refreshUserOperationExecutor.execute(operations) + + // Then - Verify success and that timezone is set to our mocked value (via update() call) + response.result shouldBe ExecutionResult.SUCCESS + coVerify(exactly = 1) { + mockPropertiesModelStore.replace( + withArg { + it.country shouldBe "US" + // Verify timezone is set to our mocked timezone (what update() does) + it.timezone shouldBe mockTimeZone + }, + ModelChangeTags.HYDRATE, + ) + } + } finally { + // Clean up the mock + unmockkObject(TimeUtils) + } + } })