Skip to content

Commit a4a6d5e

Browse files
committed
Add tests for timezone
* Add UserManagerTests that onFocus will update timezone * Update an existing test in RefreshUserOperationExecutorTests that tests a successful getUser request and hydration already. Let's add on the timezone component to this hydration, which is that timezone is no longer hydrated from the server but set locally. * Add TimeUtilsTest class testing getTimeZoneId
1 parent f225c32 commit a4a6d5e

File tree

3 files changed

+155
-49
lines changed

3 files changed

+155
-49
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.onesignal.common
2+
3+
import android.os.Build
4+
import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest
5+
import io.kotest.core.spec.style.FunSpec
6+
import io.kotest.matchers.shouldBe
7+
import io.kotest.matchers.shouldNotBe
8+
import io.kotest.matchers.string.shouldNotBeEmpty
9+
import io.kotest.matchers.string.shouldNotContain
10+
import java.time.ZoneId
11+
import java.util.TimeZone
12+
13+
@RobolectricTest
14+
class TimeUtilsTest : FunSpec({
15+
16+
test("getTimeZoneId returns correct time zone id") {
17+
// Given
18+
val expected =
19+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
20+
ZoneId.systemDefault().id
21+
} else {
22+
TimeZone.getDefault().id
23+
}
24+
25+
// When
26+
val actual = TimeUtils.getTimeZoneId()
27+
28+
// Then
29+
actual shouldBe expected
30+
actual.shouldNotBeEmpty()
31+
}
32+
33+
test("getTimeZoneId returns valid timezone format") {
34+
// When
35+
val timeZoneId = TimeUtils.getTimeZoneId()
36+
37+
// Then
38+
timeZoneId.shouldNotBeEmpty()
39+
timeZoneId shouldNotBe ""
40+
41+
// Valid timezone IDs follow IANA format patterns:
42+
// - Continental zones: "America/New_York", "Europe/London"
43+
// - UTC variants: "UTC", "GMT"
44+
// - Offset formats: "GMT+05:30", "UTC-08:00"
45+
// Should not contain spaces or invalid characters
46+
timeZoneId.shouldNotContain(" ")
47+
}
48+
})

OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.onesignal.user.internal
22

3+
import com.onesignal.common.TimeUtils
34
import com.onesignal.core.internal.language.ILanguageContext
45
import com.onesignal.mocks.MockHelper
56
import com.onesignal.user.internal.subscriptions.ISubscriptionManager
@@ -10,8 +11,10 @@ import io.kotest.matchers.shouldNotBe
1011
import io.mockk.every
1112
import io.mockk.just
1213
import io.mockk.mockk
14+
import io.mockk.mockkObject
1315
import io.mockk.runs
1416
import io.mockk.slot
17+
import io.mockk.unmockkObject
1518
import io.mockk.verify
1619

1720
class UserManagerTests : FunSpec({
@@ -192,4 +195,36 @@ class UserManagerTests : FunSpec({
192195
verify(exactly = 1) { mockSubscriptionManager.addSmsSubscription("+15558675309") }
193196
verify(exactly = 1) { mockSubscriptionManager.removeSmsSubscription("+15558675309") }
194197
}
198+
199+
test("onFocus updates timezone") {
200+
// Given
201+
val mockTimeZone = "Europe/Foo"
202+
mockkObject(TimeUtils)
203+
every { TimeUtils.getTimeZoneId() } returns mockTimeZone
204+
205+
val mockPropertiesModelStore = MockHelper.propertiesModelStore()
206+
207+
val userManager =
208+
UserManager(
209+
mockk<ISubscriptionManager>(),
210+
MockHelper.identityModelStore(),
211+
mockPropertiesModelStore,
212+
MockHelper.languageContext(),
213+
MockHelper.applicationService(),
214+
)
215+
216+
val propertiesModel = mockPropertiesModelStore.model
217+
propertiesModel.timezone shouldNotBe mockTimeZone
218+
219+
try {
220+
// When
221+
userManager.onFocus(firedOnSubscribe = false)
222+
223+
// Then
224+
propertiesModel.timezone shouldBe mockTimeZone
225+
} finally {
226+
// Clean up the mock
227+
unmockkObject(TimeUtils)
228+
}
229+
}
195230
})

OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt

Lines changed: 72 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.onesignal.user.internal.operations
22

3+
import com.onesignal.common.TimeUtils
34
import com.onesignal.common.exceptions.BackendException
45
import com.onesignal.common.modeling.ModelChangeTags
56
import com.onesignal.core.internal.operations.ExecutionResult
@@ -27,7 +28,9 @@ import io.mockk.coVerify
2728
import io.mockk.every
2829
import io.mockk.just
2930
import io.mockk.mockk
31+
import io.mockk.mockkObject
3032
import io.mockk.runs
33+
import io.mockk.unmockkObject
3134

3235
class RefreshUserOperationExecutorTests : FunSpec({
3336
val appId = "appId"
@@ -37,13 +40,24 @@ class RefreshUserOperationExecutorTests : FunSpec({
3740
val remoteSubscriptionId1 = "remote-subscriptionId1"
3841
val remoteSubscriptionId2 = "remote-subscriptionId2"
3942

40-
test("refresh user is successful") {
43+
test("refresh user is successful and models are hydrated properly") {
4144
// Given
45+
val localTimeZone = "Europe/Local"
46+
val remoteTimeZone = "Europe/Remote"
47+
mockkObject(TimeUtils)
48+
every { TimeUtils.getTimeZoneId() } returns localTimeZone
49+
50+
val localCountry = "US"
51+
val remoteCountry = "VT"
52+
val localLanguage = "fr"
53+
val remoteLanguage = "it"
54+
val remoteTags = mapOf("tagKey1" to "remote-1", "tagKey2" to "remote-2")
55+
4256
val mockUserBackendService = mockk<IUserBackendService>()
4357
coEvery { mockUserBackendService.getUser(appId, IdentityConstants.ONESIGNAL_ID, remoteOneSignalId) } returns
4458
CreateUserResponse(
4559
mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId, "aliasLabel1" to "aliasValue1"),
46-
PropertiesObject(country = "US"),
60+
PropertiesObject(country = remoteCountry, language = remoteLanguage, timezoneId = remoteTimeZone, tags = remoteTags),
4761
listOf(
4862
SubscriptionObject(existingSubscriptionId1, SubscriptionObjectType.ANDROID_PUSH, enabled = true, token = "on-backend-push-token"),
4963
SubscriptionObject(remoteSubscriptionId1, SubscriptionObjectType.ANDROID_PUSH, enabled = true, token = "pushToken2"),
@@ -61,8 +75,8 @@ class RefreshUserOperationExecutorTests : FunSpec({
6175
val mockPropertiesModelStore = MockHelper.propertiesModelStore()
6276
val mockPropertiesModel = PropertiesModel()
6377
mockPropertiesModel.onesignalId = remoteOneSignalId
64-
mockPropertiesModel.country = "VT"
65-
mockPropertiesModel.language = "language"
78+
mockPropertiesModel.country = localCountry
79+
mockPropertiesModel.language = localLanguage
6680
every { mockPropertiesModelStore.model } returns mockPropertiesModel
6781
every { mockPropertiesModelStore.replace(any(), any()) } just runs
6882

@@ -84,7 +98,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
8498

8599
val mockBuildUserService = mockk<IRebuildUserService>()
86100

87-
val loginUserOperationExecutor =
101+
val refreshUserOperationExecutor =
88102
RefreshUserOperationExecutor(
89103
mockUserBackendService,
90104
mockIdentityModelStore,
@@ -97,40 +111,49 @@ class RefreshUserOperationExecutorTests : FunSpec({
97111

98112
val operations = listOf<Operation>(RefreshUserOperation(appId, remoteOneSignalId))
99113

100-
// When
101-
val response = loginUserOperationExecutor.execute(operations)
102-
103-
// Then
104-
response.result shouldBe ExecutionResult.SUCCESS
105-
coVerify(exactly = 1) {
106-
mockUserBackendService.getUser(appId, IdentityConstants.ONESIGNAL_ID, remoteOneSignalId)
107-
mockIdentityModelStore.replace(
108-
withArg {
109-
it["aliasLabel1"] shouldBe "aliasValue1"
110-
},
111-
ModelChangeTags.HYDRATE,
112-
)
113-
mockPropertiesModelStore.replace(
114-
withArg {
115-
it.country shouldBe "US"
116-
it.language shouldBe null
117-
},
118-
ModelChangeTags.HYDRATE,
119-
)
120-
mockSubscriptionsModelStore.replaceAll(
121-
withArg {
122-
it.count() shouldBe 2
123-
it[0].id shouldBe remoteSubscriptionId2
124-
it[0].type shouldBe SubscriptionType.EMAIL
125-
it[0].optedIn shouldBe true
126-
it[0].address shouldBe "name@company.com"
127-
it[1].id shouldBe existingSubscriptionId1
128-
it[1].type shouldBe SubscriptionType.PUSH
129-
it[1].optedIn shouldBe true
130-
it[1].address shouldBe onDevicePushToken
131-
},
132-
ModelChangeTags.HYDRATE,
133-
)
114+
try {
115+
// When
116+
val response = refreshUserOperationExecutor.execute(operations)
117+
118+
// Then
119+
response.result shouldBe ExecutionResult.SUCCESS
120+
coVerify(exactly = 1) {
121+
mockUserBackendService.getUser(appId, IdentityConstants.ONESIGNAL_ID, remoteOneSignalId)
122+
mockIdentityModelStore.replace(
123+
withArg {
124+
it["aliasLabel1"] shouldBe "aliasValue1"
125+
},
126+
ModelChangeTags.HYDRATE,
127+
)
128+
// The properties model should be set with appropriate remote and local values
129+
mockPropertiesModelStore.replace(
130+
withArg {
131+
it.onesignalId shouldBe remoteOneSignalId
132+
it.country shouldBe remoteCountry
133+
it.language shouldBe remoteLanguage
134+
it.tags shouldBe remoteTags
135+
it.timezone shouldBe localTimeZone // timezone is set locally
136+
},
137+
ModelChangeTags.HYDRATE,
138+
)
139+
mockSubscriptionsModelStore.replaceAll(
140+
withArg {
141+
it.count() shouldBe 2
142+
it[0].id shouldBe remoteSubscriptionId2
143+
it[0].type shouldBe SubscriptionType.EMAIL
144+
it[0].optedIn shouldBe true
145+
it[0].address shouldBe "name@company.com"
146+
it[1].id shouldBe existingSubscriptionId1
147+
it[1].type shouldBe SubscriptionType.PUSH
148+
it[1].optedIn shouldBe true
149+
it[1].address shouldBe onDevicePushToken
150+
},
151+
ModelChangeTags.HYDRATE,
152+
)
153+
}
154+
} finally {
155+
// Clean up the mock
156+
unmockkObject(TimeUtils)
134157
}
135158
}
136159

@@ -159,7 +182,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
159182
val mockSubscriptionsModelStore = mockk<SubscriptionModelStore>()
160183
val mockBuildUserService = mockk<IRebuildUserService>()
161184

162-
val loginUserOperationExecutor =
185+
val refreshUserOperationExecutor =
163186
RefreshUserOperationExecutor(
164187
mockUserBackendService,
165188
mockIdentityModelStore,
@@ -173,7 +196,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
173196
val operations = listOf<Operation>(RefreshUserOperation(appId, remoteOneSignalId))
174197

175198
// When
176-
val response = loginUserOperationExecutor.execute(operations)
199+
val response = refreshUserOperationExecutor.execute(operations)
177200

178201
// Then
179202
response.result shouldBe ExecutionResult.SUCCESS
@@ -198,7 +221,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
198221
val mockSubscriptionsModelStore = mockk<SubscriptionModelStore>()
199222
val mockBuildUserService = mockk<IRebuildUserService>()
200223

201-
val loginUserOperationExecutor =
224+
val refreshUserOperationExecutor =
202225
RefreshUserOperationExecutor(
203226
mockUserBackendService,
204227
mockIdentityModelStore,
@@ -212,7 +235,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
212235
val operations = listOf<Operation>(RefreshUserOperation(appId, remoteOneSignalId))
213236

214237
// When
215-
val response = loginUserOperationExecutor.execute(operations)
238+
val response = refreshUserOperationExecutor.execute(operations)
216239

217240
// Then
218241
response.result shouldBe ExecutionResult.FAIL_RETRY
@@ -233,7 +256,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
233256
val mockSubscriptionsModelStore = mockk<SubscriptionModelStore>()
234257
val mockBuildUserService = mockk<IRebuildUserService>()
235258

236-
val loginUserOperationExecutor =
259+
val refreshUserOperationExecutor =
237260
RefreshUserOperationExecutor(
238261
mockUserBackendService,
239262
mockIdentityModelStore,
@@ -247,7 +270,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
247270
val operations = listOf<Operation>(RefreshUserOperation(appId, remoteOneSignalId))
248271

249272
// When
250-
val response = loginUserOperationExecutor.execute(operations)
273+
val response = refreshUserOperationExecutor.execute(operations)
251274

252275
// Then
253276
response.result shouldBe ExecutionResult.FAIL_NORETRY
@@ -268,7 +291,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
268291
val mockBuildUserService = mockk<IRebuildUserService>()
269292
every { mockBuildUserService.getRebuildOperationsIfCurrentUser(any(), any()) } returns null
270293

271-
val loginUserOperationExecutor =
294+
val refreshUserOperationExecutor =
272295
RefreshUserOperationExecutor(
273296
mockUserBackendService,
274297
mockIdentityModelStore,
@@ -282,7 +305,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
282305
val operations = listOf<Operation>(RefreshUserOperation(appId, remoteOneSignalId))
283306

284307
// When
285-
val response = loginUserOperationExecutor.execute(operations)
308+
val response = refreshUserOperationExecutor.execute(operations)
286309

287310
// Then
288311
response.result shouldBe ExecutionResult.FAIL_NORETRY
@@ -305,7 +328,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
305328
val mockConfigModelStore = MockHelper.configModelStore().also { it.model.opRepoPostCreateRetryUpTo = 1_000 }
306329
val newRecordState = getNewRecordState(mockConfigModelStore).also { it.add(remoteOneSignalId) }
307330

308-
val loginUserOperationExecutor =
331+
val refreshUserOperationExecutor =
309332
RefreshUserOperationExecutor(
310333
mockUserBackendService,
311334
mockIdentityModelStore,
@@ -319,7 +342,7 @@ class RefreshUserOperationExecutorTests : FunSpec({
319342
val operations = listOf<Operation>(RefreshUserOperation(appId, remoteOneSignalId))
320343

321344
// When
322-
val response = loginUserOperationExecutor.execute(operations)
345+
val response = refreshUserOperationExecutor.execute(operations)
323346

324347
// Then
325348
response.result shouldBe ExecutionResult.FAIL_RETRY

0 commit comments

Comments
 (0)