@@ -1075,6 +1075,34 @@ describe('RewardsController', () => {
10751075 ) ;
10761076 } ) ;
10771077 } ) ;
1078+
1079+ it ( 'should throw AuthorizationFailedError when subscription token is missing' , async ( ) => {
1080+ const state : Partial < RewardsControllerState > = {
1081+ rewardsSeasons : {
1082+ [ MOCK_SEASON_ID ] : {
1083+ id : MOCK_SEASON_ID ,
1084+ name : 'Season 1' ,
1085+ startDate : new Date ( '2024-01-01' ) . getTime ( ) ,
1086+ endDate : new Date ( '2024-12-31' ) . getTime ( ) ,
1087+ tiers : MOCK_SEASON_TIERS ,
1088+ } ,
1089+ } ,
1090+ } ;
1091+
1092+ await withController (
1093+ { state, isDisabled : false } ,
1094+ async ( { controller } ) => {
1095+ await expect (
1096+ controller . getSeasonStatus ( MOCK_SUBSCRIPTION_ID , MOCK_SEASON_ID ) ,
1097+ ) . rejects . toThrow (
1098+ `No subscription token found for subscription ID: ${ MOCK_SUBSCRIPTION_ID } ` ,
1099+ ) ;
1100+ await expect (
1101+ controller . getSeasonStatus ( MOCK_SUBSCRIPTION_ID , MOCK_SEASON_ID ) ,
1102+ ) . rejects . toBeInstanceOf ( AuthorizationFailedError ) ;
1103+ } ,
1104+ ) ;
1105+ } ) ;
10781106 } ) ;
10791107
10801108 describe ( 'optIn' , ( ) => {
@@ -1676,6 +1704,102 @@ describe('RewardsController', () => {
16761704 expect ( result . addressesNeedingFresh ) . toEqual ( [ MOCK_ACCOUNT_ADDRESS ] ) ;
16771705 } ) ;
16781706 } ) ;
1707+
1708+ it ( 'should force fresh check for not-opted-in accounts checked more than 5 minutes ago' , async ( ) => {
1709+ const state : Partial < RewardsControllerState > = {
1710+ rewardsAccounts : {
1711+ [ MOCK_CAIP_ACCOUNT ] : {
1712+ account : MOCK_CAIP_ACCOUNT ,
1713+ hasOptedIn : false ,
1714+ subscriptionId : null ,
1715+ perpsFeeDiscount : null ,
1716+ lastPerpsDiscountRateFetched : null ,
1717+ lastFreshOptInStatusCheck : Date . now ( ) - 1000 * 60 * 6 , // 6 minutes ago (exceeds 5 minute threshold)
1718+ } ,
1719+ } ,
1720+ } ;
1721+
1722+ await withController ( { state, isDisabled : false } , ( { controller } ) => {
1723+ const addressToAccountMap = new Map < string , InternalAccount > ( ) ;
1724+ addressToAccountMap . set (
1725+ MOCK_ACCOUNT_ADDRESS . toLowerCase ( ) ,
1726+ MOCK_INTERNAL_ACCOUNT ,
1727+ ) ;
1728+
1729+ const result = controller . checkOptInStatusAgainstCache (
1730+ [ MOCK_ACCOUNT_ADDRESS ] ,
1731+ addressToAccountMap ,
1732+ ) ;
1733+
1734+ expect ( result . cachedOptInResults ) . toEqual ( [ null ] ) ;
1735+ expect ( result . cachedSubscriptionIds ) . toEqual ( [ null ] ) ;
1736+ expect ( result . addressesNeedingFresh ) . toEqual ( [ MOCK_ACCOUNT_ADDRESS ] ) ;
1737+ } ) ;
1738+ } ) ;
1739+
1740+ it ( 'should use cached data for not-opted-in accounts checked within 5 minutes' , async ( ) => {
1741+ const state : Partial < RewardsControllerState > = {
1742+ rewardsAccounts : {
1743+ [ MOCK_CAIP_ACCOUNT ] : {
1744+ account : MOCK_CAIP_ACCOUNT ,
1745+ hasOptedIn : false ,
1746+ subscriptionId : null ,
1747+ perpsFeeDiscount : null ,
1748+ lastPerpsDiscountRateFetched : null ,
1749+ lastFreshOptInStatusCheck : Date . now ( ) - 1000 * 60 * 2 , // 2 minutes ago (within 5 minute threshold)
1750+ } ,
1751+ } ,
1752+ } ;
1753+
1754+ await withController ( { state, isDisabled : false } , ( { controller } ) => {
1755+ const addressToAccountMap = new Map < string , InternalAccount > ( ) ;
1756+ addressToAccountMap . set (
1757+ MOCK_ACCOUNT_ADDRESS . toLowerCase ( ) ,
1758+ MOCK_INTERNAL_ACCOUNT ,
1759+ ) ;
1760+
1761+ const result = controller . checkOptInStatusAgainstCache (
1762+ [ MOCK_ACCOUNT_ADDRESS ] ,
1763+ addressToAccountMap ,
1764+ ) ;
1765+
1766+ expect ( result . cachedOptInResults ) . toEqual ( [ false ] ) ;
1767+ expect ( result . cachedSubscriptionIds ) . toEqual ( [ null ] ) ;
1768+ expect ( result . addressesNeedingFresh ) . toEqual ( [ ] ) ;
1769+ } ) ;
1770+ } ) ;
1771+
1772+ it ( 'should force fresh check for not-opted-in accounts without lastFreshOptInStatusCheck' , async ( ) => {
1773+ const state : Partial < RewardsControllerState > = {
1774+ rewardsAccounts : {
1775+ [ MOCK_CAIP_ACCOUNT ] : {
1776+ account : MOCK_CAIP_ACCOUNT ,
1777+ hasOptedIn : false ,
1778+ subscriptionId : null ,
1779+ perpsFeeDiscount : null ,
1780+ lastPerpsDiscountRateFetched : null ,
1781+ lastFreshOptInStatusCheck : undefined ,
1782+ } ,
1783+ } ,
1784+ } ;
1785+
1786+ await withController ( { state, isDisabled : false } , ( { controller } ) => {
1787+ const addressToAccountMap = new Map < string , InternalAccount > ( ) ;
1788+ addressToAccountMap . set (
1789+ MOCK_ACCOUNT_ADDRESS . toLowerCase ( ) ,
1790+ MOCK_INTERNAL_ACCOUNT ,
1791+ ) ;
1792+
1793+ const result = controller . checkOptInStatusAgainstCache (
1794+ [ MOCK_ACCOUNT_ADDRESS ] ,
1795+ addressToAccountMap ,
1796+ ) ;
1797+
1798+ expect ( result . cachedOptInResults ) . toEqual ( [ null ] ) ;
1799+ expect ( result . cachedSubscriptionIds ) . toEqual ( [ null ] ) ;
1800+ expect ( result . addressesNeedingFresh ) . toEqual ( [ MOCK_ACCOUNT_ADDRESS ] ) ;
1801+ } ) ;
1802+ } ) ;
16791803 } ) ;
16801804
16811805 describe ( 'shouldSkipSilentAuth' , ( ) => {
@@ -1724,6 +1848,30 @@ describe('RewardsController', () => {
17241848 } ) ;
17251849 } ) ;
17261850
1851+ it ( 'should skip for not-opted-in accounts checked within 5 minutes' , async ( ) => {
1852+ const state : Partial < RewardsControllerState > = {
1853+ rewardsAccounts : {
1854+ [ MOCK_CAIP_ACCOUNT ] : {
1855+ account : MOCK_CAIP_ACCOUNT ,
1856+ hasOptedIn : false ,
1857+ subscriptionId : null ,
1858+ perpsFeeDiscount : null ,
1859+ lastPerpsDiscountRateFetched : null ,
1860+ lastFreshOptInStatusCheck : Date . now ( ) - 1000 * 60 * 2 , // 2 minutes ago (within 5 minute threshold)
1861+ } ,
1862+ } ,
1863+ } ;
1864+
1865+ await withController ( { state, isDisabled : false } , ( { controller } ) => {
1866+ const result = controller . shouldSkipSilentAuth (
1867+ MOCK_CAIP_ACCOUNT ,
1868+ MOCK_INTERNAL_ACCOUNT ,
1869+ ) ;
1870+
1871+ expect ( result ) . toBe ( true ) ;
1872+ } ) ;
1873+ } ) ;
1874+
17271875 it ( 'should not skip for stale not-opted-in accounts' , async ( ) => {
17281876 const state : Partial < RewardsControllerState > = {
17291877 rewardsAccounts : {
@@ -1733,7 +1881,7 @@ describe('RewardsController', () => {
17331881 subscriptionId : null ,
17341882 perpsFeeDiscount : null ,
17351883 lastPerpsDiscountRateFetched : null ,
1736- lastFreshOptInStatusCheck : Date . now ( ) - 1000 * 60 * 60 * 24 * 2 , // 2 days ago
1884+ lastFreshOptInStatusCheck : Date . now ( ) - 1000 * 60 * 6 , // 6 minutes ago (exceeds 5 minute threshold)
17371885 } ,
17381886 } ,
17391887 } ;
@@ -2532,11 +2680,14 @@ describe('Additional RewardsController edge cases', () => {
25322680 } ) ;
25332681
25342682 describe ( 'getCandidateSubscriptionId - error scenarios' , ( ) => {
2535- it ( 'should return subscription ID from cache if session token exists ' , async ( ) => {
2683+ it ( 'should return subscription ID from cache if session token and subscription exist ' , async ( ) => {
25362684 const state : Partial < RewardsControllerState > = {
25372685 rewardsSubscriptionTokens : {
25382686 [ MOCK_SUBSCRIPTION_ID ] : MOCK_SESSION_TOKEN ,
25392687 } ,
2688+ rewardsSubscriptions : {
2689+ [ MOCK_SUBSCRIPTION_ID ] : MOCK_SUBSCRIPTION ,
2690+ } ,
25402691 } ;
25412692
25422693 await withController (
@@ -2579,6 +2730,42 @@ describe('Additional RewardsController edge cases', () => {
25792730 } ,
25802731 ) ;
25812732 } ) ;
2733+
2734+ it ( 'should continue to silent auth when subscription exists but session token is missing' , async ( ) => {
2735+ const state : Partial < RewardsControllerState > = {
2736+ rewardsSubscriptions : {
2737+ [ MOCK_SUBSCRIPTION_ID ] : MOCK_SUBSCRIPTION ,
2738+ } ,
2739+ } ;
2740+
2741+ await withController (
2742+ { state, isDisabled : false } ,
2743+ async ( { controller, mockMessengerCall } ) => {
2744+ mockMessengerCall . mockImplementation ( ( actionType ) => {
2745+ if ( actionType === 'AccountsController:listMultichainAccounts' ) {
2746+ return [ MOCK_INTERNAL_ACCOUNT ] ;
2747+ }
2748+ if ( actionType === 'RewardsDataService:getOptInStatus' ) {
2749+ return Promise . resolve ( {
2750+ ois : [ true ] ,
2751+ sids : [ MOCK_SUBSCRIPTION_ID ] ,
2752+ } ) ;
2753+ }
2754+ if ( actionType === 'KeyringController:signPersonalMessage' ) {
2755+ return Promise . resolve ( '0xmocksignature' ) ;
2756+ }
2757+ if ( actionType === 'RewardsDataService:login' ) {
2758+ return Promise . resolve ( MOCK_LOGIN_RESPONSE ) ;
2759+ }
2760+ return undefined ;
2761+ } ) ;
2762+
2763+ const result = await controller . getCandidateSubscriptionId ( ) ;
2764+
2765+ expect ( result ) . toBe ( MOCK_SUBSCRIPTION_ID ) ;
2766+ } ,
2767+ ) ;
2768+ } ) ;
25822769 } ) ;
25832770
25842771 describe ( 'linkAccountsToSubscriptionCandidate - error handling' , ( ) => {
0 commit comments