@@ -16,6 +16,7 @@ import org.junit.jupiter.api.BeforeEach
1616import org.junit.jupiter.api.Test
1717import org.mockito.ArgumentCaptor
1818import org.mockito.ArgumentMatchers.any
19+ import org.mockito.ArgumentMatchers.anyBoolean
1920import org.mockito.ArgumentMatchers.anyLong
2021import org.mockito.ArgumentMatchers.eq
2122import org.mockito.Mockito.mock
@@ -59,6 +60,7 @@ class LicenseEnforcerTest {
5960 `when `(sharedPreferencesHandler.licenseToken()).thenReturn(" " )
6061 `when `(sharedPreferencesHandler.hasRunningSubscription()).thenReturn(false )
6162 `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(System .currentTimeMillis() + 86400000L )
63+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(false )
6264
6365 assertTrue(licenseEnforcer.hasWriteAccess())
6466 }
@@ -69,6 +71,7 @@ class LicenseEnforcerTest {
6971 `when `(sharedPreferencesHandler.licenseToken()).thenReturn(" " )
7072 `when `(sharedPreferencesHandler.hasRunningSubscription()).thenReturn(false )
7173 `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(System .currentTimeMillis() - 1000L )
74+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(false )
7275
7376 assertFalse(licenseEnforcer.hasWriteAccess())
7477 }
@@ -150,6 +153,7 @@ class LicenseEnforcerTest {
150153 `when `(sharedPreferencesHandler.licenseToken()).thenReturn(" " )
151154 `when `(sharedPreferencesHandler.hasRunningSubscription()).thenReturn(false )
152155 `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(System .currentTimeMillis() + 86400000L )
156+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(false )
153157
154158 assertTrue(licenseEnforcer.hasWriteAccessForVault(vault))
155159 }
@@ -260,13 +264,15 @@ class LicenseEnforcerTest {
260264 @Test
261265 fun `hasActiveTrial returns true when trial expiration is in the future` () {
262266 `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(System .currentTimeMillis() + 86400000L )
267+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(false )
263268
264269 assertTrue(licenseEnforcer.hasActiveTrial())
265270 }
266271
267272 @Test
268273 fun `hasActiveTrial returns false when trial expiration is in the past` () {
269274 `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(System .currentTimeMillis() - 1000L )
275+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(false )
270276
271277 assertFalse(licenseEnforcer.hasActiveTrial())
272278 }
@@ -319,6 +325,7 @@ class LicenseEnforcerTest {
319325 fun `evaluateTrialState returns active with formatted date when trial is active` () {
320326 val futureDate = System .currentTimeMillis() + 86400000L
321327 `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(futureDate)
328+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(false )
322329
323330 val state = licenseEnforcer.evaluateTrialState()
324331
@@ -330,6 +337,7 @@ class LicenseEnforcerTest {
330337 @Test
331338 fun `evaluateTrialState returns expired with formatted date when trial is expired` () {
332339 `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(System .currentTimeMillis() - 1000L )
340+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(false )
333341
334342 val state = licenseEnforcer.evaluateTrialState()
335343
@@ -349,6 +357,85 @@ class LicenseEnforcerTest {
349357 assertNull(state.formattedExpirationDate)
350358 }
351359
360+ // -- sticky trial expiry --
361+
362+ @Test
363+ fun `hasActiveTrial latches isTrialExpired when expiration is in the past` () {
364+ `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(System .currentTimeMillis() - 1000L )
365+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(false )
366+
367+ assertFalse(licenseEnforcer.hasActiveTrial())
368+ verify(sharedPreferencesHandler).setTrialExpired(true )
369+ }
370+
371+ @Test
372+ fun `hasActiveTrial returns false when isTrialExpired is already true even if date is in the future` () {
373+ `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(System .currentTimeMillis() + 86400000L )
374+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(true )
375+
376+ assertFalse(licenseEnforcer.hasActiveTrial())
377+ verify(sharedPreferencesHandler, never()).setTrialExpired(anyBoolean())
378+ }
379+
380+ @Test
381+ fun `evaluateTrialState latches isTrialExpired and returns expired when date is in the past` () {
382+ `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(System .currentTimeMillis() - 1000L )
383+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(false )
384+
385+ val state = licenseEnforcer.evaluateTrialState()
386+
387+ assertFalse(state.isActive)
388+ assertTrue(state.isExpired)
389+ verify(sharedPreferencesHandler).setTrialExpired(true )
390+ }
391+
392+ @Test
393+ fun `evaluateTrialState returns expired when isTrialExpired is already true even if date is in the future` () {
394+ `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(System .currentTimeMillis() + 86400000L )
395+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(true )
396+
397+ val state = licenseEnforcer.evaluateTrialState()
398+
399+ assertFalse(state.isActive)
400+ assertTrue(state.isExpired)
401+ assertNotNull(state.formattedExpirationDate)
402+ verify(sharedPreferencesHandler, never()).setTrialExpired(anyBoolean())
403+ }
404+
405+ @Test
406+ fun `observeTrialExpiry idempotent once flag is set` () {
407+ `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(System .currentTimeMillis() - 1000L )
408+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(true )
409+
410+ licenseEnforcer.hasActiveTrial()
411+ licenseEnforcer.evaluateTrialState()
412+
413+ verify(sharedPreferencesHandler, never()).setTrialExpired(anyBoolean())
414+ }
415+
416+ @Test
417+ fun `startTrial does not reset sticky trialExpired flag` () {
418+ `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(0L )
419+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(true )
420+
421+ licenseEnforcer.startTrial()
422+
423+ verify(sharedPreferencesHandler, never()).setTrialExpired(anyBoolean())
424+ }
425+
426+ @Test
427+ fun `fresh install with no trial and no flag behaves unchanged` () {
428+ `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(0L )
429+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(false )
430+
431+ assertFalse(licenseEnforcer.hasActiveTrial())
432+ val state = licenseEnforcer.evaluateTrialState()
433+ assertFalse(state.isActive)
434+ assertFalse(state.isExpired)
435+ assertNull(state.formattedExpirationDate)
436+ verify(sharedPreferencesHandler, never()).setTrialExpired(anyBoolean())
437+ }
438+
352439 // -- evaluateUiState --
353440
354441 @Test
@@ -358,6 +445,7 @@ class LicenseEnforcerTest {
358445 `when `(sharedPreferencesHandler.licenseToken()).thenReturn(" " )
359446 `when `(sharedPreferencesHandler.hasRunningSubscription()).thenReturn(false )
360447 `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(System .currentTimeMillis() + 86400000L )
448+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(false )
361449 `when `(context.getString(eq(R .string.screen_license_check_trial_expiration), any())).thenReturn(" Expiration Date: Mar 28, 2026" )
362450
363451 val uiState = licenseEnforcer.evaluateUiState(context)
@@ -375,6 +463,7 @@ class LicenseEnforcerTest {
375463 `when `(sharedPreferencesHandler.licenseToken()).thenReturn(" " )
376464 `when `(sharedPreferencesHandler.hasRunningSubscription()).thenReturn(false )
377465 `when `(sharedPreferencesHandler.trialExpirationDate()).thenReturn(System .currentTimeMillis() - 1000L )
466+ `when `(sharedPreferencesHandler.isTrialExpired()).thenReturn(false )
378467 `when `(context.getString(eq(R .string.screen_license_check_trial_expiration), any())).thenReturn(" Expiration Date: Mar 26, 2026" )
379468
380469 val uiState = licenseEnforcer.evaluateUiState(context)
0 commit comments