@@ -5,14 +5,18 @@ import * as AppAuthModule from 'react-native-app-auth';
55import * as OidcAuthStateStorageModule from './OidcAuthStateStorage' ;
66import type { ContentpassState } from './types/ContentpassState' ;
77import OidcAuthStateStorage from './OidcAuthStateStorage' ;
8- import * as FetchContentpassTokenModule from './utils /fetchContentpassToken' ;
8+ import * as FetchContentpassTokenModule from './contentpassTokenUtils /fetchContentpassToken' ;
99import { SCOPES } from './consts/oidcConsts' ;
1010import * as SentryIntegrationModule from './sentryIntegration' ;
11+ import * as SendStatsModule from './countImpressionUtils/sendStats' ;
12+ import * as SendPageViewEventModule from './countImpressionUtils/sendPageViewEvent' ;
1113
1214const config : ContentpassConfig = {
13- propertyId : 'propertyId-1 ' ,
15+ propertyId : '5803179c-5b9f-40be-9a91-e67e8ea20593 ' ,
1416 redirectUrl : 'de.test.net://oauth' ,
1517 issuer : 'https://issuer.net' ,
18+ apiUrl : 'https://cp.property.com' ,
19+ samplingRate : 1 ,
1620} ;
1721
1822const NOW = new Date ( '2024-12-02T11:53:56.272Z' ) . getTime ( ) ;
@@ -36,13 +40,21 @@ const EXAMPLE_REFRESH_RESULT = {
3640 accessTokenExpirationDate : '2024-12-03T10:00:50Z' ,
3741} ;
3842
43+ const CONTENTPASS_TOKEN_WITH_PLANS =
44+ 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjY5NzUwYTZjLTNmYjctNDUyNi05NWY4LTVhZmYxMmIyZjFjOSJ9.eyJhdXRoIjp0cnVlLCJ0eXBlIjoiY3AiLCJwbGFucyI6WyIwYWNhZTkxNy1iZTk5LTQ4ZWEtYjhmMS0yMGZhNjhhNDdkM2EiLCI0NDIxNjI4Yy05NjA2LTRjMDEtOGU1ZC1jMmE5YmNhNjhhYjQiLCI3ZThkZTBjYy0zZTk3LTQ5YTItODgxZC05ZmZiNWI4NDE1MTUiLCJhNDcyMWRiNS02N2RmLTQxNDUtYmJiZi1jYmQwOWY3ZTAzOTciLCJjNGQzYjBmNS05ODlhLTRmN2ItOGFjNy0zZDhmZmE5NTcxN2YiLCI2NGRkOTkwNS05NmUxLTRmYjItOTgwZC01MDdmMTYzNzVmZTkiXSwiYXVkIjoiY2MzZmM0YWQiLCJpYXQiOjE3MzMxMzU2ODEsImV4cCI6MTczMzMxMjA4MX0.CMtH7HRLf2HVgw3_cZRN0en8tml_SQKM73iLGJAp72-vJuRJaq85xBp6Jgy9WD3L7x4itRlBAYZxX8tLxZGogU0WP4_dMGFQ2QlcwKshwJygwRM1YqvxGWX2Az_KxEMc2QGHvpE1qe2MAr_xOU7VFfc0-vWxFc3hRzpAM5j7YHctj2t1v6h9-M7V2Hkcn37569QmtgU8gJkUxXsgUTufbb1ikjjjAvnjvTluHJo51_utbimpUbCk3EFxXVCVEI_pAqiZQXNninUQ6dbSujLb3L2UlEdQzLeUiBdYroeFzSyruLrR841ledLQ5ZP2OqzF5oUMuAGVOOhmgGdwGMCDRQ' ;
45+
46+ export const CONTENTPASS_TOKEN_WITHOUT_PLANS =
47+ 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjY5NzUwYTZjLTNmYjctNDUyNi05NWY4LTVhZmYxMmIyZjFjOSJ9.eyJhdXRoIjp0cnVlLCJ0eXBlIjoiY3AiLCJwbGFucyI6W10sImF1ZCI6ImNjM2ZjNGFkIiwiaWF0IjoxNzMzMTM1NjgxLCJleHAiOjE3MzMzMTIwODF9.CMtH7HRLf2HVgw3_cZRN0en8tml_SQKM73iLGJAp72-vJuRJaq85xBp6Jgy9WD3L7x4itRlBAYZxX8tLxZGogU0WP4_dMGFQ2QlcwKshwJygwRM1YqvxGWX2Az_KxEMc2QGHvpE1qe2MAr_xOU7VFfc0-vWxFc3hRzpAM5j7YHctj2t1v6h9-M7V2Hkcn37569QmtgU8gJkUxXsgUTufbb1ikjjjAvnjvTluHJo51_utbimpUbCk3EFxXVCVEI_pAqiZQXNninUQ6dbSujLb3L2UlEdQzLeUiBdYroeFzSyruLrR841ledLQ5ZP2OqzF5oUMuAGVOOhmgGdwGMCDRQ' ;
48+
3949describe ( 'Contentpass' , ( ) => {
4050 let contentpass : Contentpass ;
4151 let authorizeSpy : jest . SpyInstance ;
4252 let refreshSpy : jest . SpyInstance ;
4353 let reportErrorSpy : jest . SpyInstance ;
4454 let fetchContentpassTokenSpy : jest . SpyInstance ;
4555 let oidcAuthStorageMock : OidcAuthStateStorage ;
56+ let sendStatsSpy : jest . SpyInstance ;
57+ let sendPageViewEventSpy : jest . SpyInstance ;
4658
4759 beforeEach ( ( ) => {
4860 jest . useFakeTimers ( { now : NOW } ) ;
@@ -68,9 +80,15 @@ describe('Contentpass', () => {
6880
6981 fetchContentpassTokenSpy = jest
7082 . spyOn ( FetchContentpassTokenModule , 'default' )
71- . mockResolvedValue (
72- 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjY5NzUwYTZjLTNmYjctNDUyNi05NWY4LTVhZmYxMmIyZjFjOSJ9.eyJhdXRoIjp0cnVlLCJ0eXBlIjoiY3AiLCJwbGFucyI6WyIwYWNhZTkxNy1iZTk5LTQ4ZWEtYjhmMS0yMGZhNjhhNDdkM2EiLCI0NDIxNjI4Yy05NjA2LTRjMDEtOGU1ZC1jMmE5YmNhNjhhYjQiLCI3ZThkZTBjYy0zZTk3LTQ5YTItODgxZC05ZmZiNWI4NDE1MTUiLCJhNDcyMWRiNS02N2RmLTQxNDUtYmJiZi1jYmQwOWY3ZTAzOTciLCJjNGQzYjBmNS05ODlhLTRmN2ItOGFjNy0zZDhmZmE5NTcxN2YiLCI2NGRkOTkwNS05NmUxLTRmYjItOTgwZC01MDdmMTYzNzVmZTkiXSwiYXVkIjoiY2MzZmM0YWQiLCJpYXQiOjE3MzMxMzU2ODEsImV4cCI6MTczMzMxMjA4MX0.CMtH7HRLf2HVgw3_cZRN0en8tml_SQKM73iLGJAp72-vJuRJaq85xBp6Jgy9WD3L7x4itRlBAYZxX8tLxZGogU0WP4_dMGFQ2QlcwKshwJygwRM1YqvxGWX2Az_KxEMc2QGHvpE1qe2MAr_xOU7VFfc0-vWxFc3hRzpAM5j7YHctj2t1v6h9-M7V2Hkcn37569QmtgU8gJkUxXsgUTufbb1ikjjjAvnjvTluHJo51_utbimpUbCk3EFxXVCVEI_pAqiZQXNninUQ6dbSujLb3L2UlEdQzLeUiBdYroeFzSyruLrR841ledLQ5ZP2OqzF5oUMuAGVOOhmgGdwGMCDRQ'
73- ) ;
83+ . mockResolvedValue ( CONTENTPASS_TOKEN_WITH_PLANS ) ;
84+
85+ sendStatsSpy = jest
86+ . spyOn ( SendStatsModule , 'default' )
87+ . mockResolvedValue ( { ok : true } as any ) ;
88+
89+ sendPageViewEventSpy = jest
90+ . spyOn ( SendPageViewEventModule , 'default' )
91+ . mockResolvedValue ( { ok : true } as any ) ;
7492
7593 contentpass = new Contentpass ( config ) ;
7694 } ) ;
@@ -82,6 +100,24 @@ describe('Contentpass', () => {
82100 } ) ;
83101
84102 describe ( 'constructor' , ( ) => {
103+ it ( 'should throw an error if sampling rate is not between 0 and 1' , ( ) => {
104+ expect (
105+ ( ) =>
106+ new Contentpass ( {
107+ ...config ,
108+ samplingRate : - 1 ,
109+ } )
110+ ) . toThrow ( 'Sampling rate must be between 0 and 1' ) ;
111+
112+ expect (
113+ ( ) =>
114+ new Contentpass ( {
115+ ...config ,
116+ samplingRate : 2 ,
117+ } )
118+ ) . toThrow ( 'Sampling rate must be between 0 and 1' ) ;
119+ } ) ;
120+
85121 it ( 'should initialise contentpass state' , ( ) => {
86122 const contentpassStates : ContentpassState [ ] = [ ] ;
87123 contentpass . registerObserver ( ( state ) => {
@@ -137,9 +173,9 @@ describe('Contentpass', () => {
137173 expect ( refreshSpy ) . toHaveBeenCalledTimes ( 1 ) ;
138174 expect ( refreshSpy ) . toHaveBeenCalledWith (
139175 {
140- clientId : ' propertyId-1' ,
141- redirectUrl : 'de.test.net://oauth' ,
142- issuer : 'https://issuer.net' ,
176+ clientId : config . propertyId ,
177+ redirectUrl : config . redirectUrl ,
178+ issuer : config . issuer ,
143179 scopes : SCOPES ,
144180 } ,
145181 { refreshToken : EXAMPLE_AUTH_RESULT . refreshToken }
@@ -158,13 +194,13 @@ describe('Contentpass', () => {
158194
159195 expect ( authorizeSpy ) . toHaveBeenCalledWith ( {
160196 additionalParameters : {
161- cp_property : ' propertyId-1' ,
197+ cp_property : config . propertyId ,
162198 cp_route : 'login' ,
163199 prompt : 'consent' ,
164200 } ,
165- clientId : ' propertyId-1' ,
166- issuer : 'https://issuer.net' ,
167- redirectUrl : 'de.test.net://oauth' ,
201+ clientId : config . propertyId ,
202+ issuer : config . issuer ,
203+ redirectUrl : config . redirectUrl ,
168204 scopes : [ 'openid' , 'offline_access' , 'contentpass' ] ,
169205 } ) ;
170206 } ) ;
@@ -248,9 +284,9 @@ describe('Contentpass', () => {
248284 expect ( refreshSpy ) . toHaveBeenCalledTimes ( 1 ) ;
249285 expect ( refreshSpy ) . toHaveBeenCalledWith (
250286 {
251- clientId : ' propertyId-1' ,
252- redirectUrl : 'de.test.net://oauth' ,
253- issuer : 'https://issuer.net' ,
287+ clientId : config . propertyId ,
288+ redirectUrl : config . redirectUrl ,
289+ issuer : config . issuer ,
254290 scopes : SCOPES ,
255291 } ,
256292 { refreshToken : EXAMPLE_AUTH_RESULT . refreshToken }
@@ -436,4 +472,78 @@ describe('Contentpass', () => {
436472 } ) ;
437473 } ) ;
438474 } ) ;
475+
476+ describe ( 'countImpression' , ( ) => {
477+ it ( 'should count paid impression if user has valid subscription and access token' , async ( ) => {
478+ await contentpass . authenticate ( ) ;
479+
480+ await contentpass . countImpression ( ) ;
481+
482+ expect ( sendPageViewEventSpy ) . toHaveBeenCalledWith ( config . apiUrl , {
483+ propertyId : config . propertyId ,
484+ impressionId : expect . any ( String ) ,
485+ accessToken : EXAMPLE_AUTH_RESULT . accessToken ,
486+ } ) ;
487+ expect ( sendStatsSpy ) . toHaveBeenCalled ( ) ;
488+ } ) ;
489+
490+ it ( 'should not count paid impression if user is authenticated, but does not have valid subscription' , async ( ) => {
491+ fetchContentpassTokenSpy . mockResolvedValue (
492+ CONTENTPASS_TOKEN_WITHOUT_PLANS
493+ ) ;
494+ await contentpass . authenticate ( ) ;
495+
496+ await contentpass . countImpression ( ) ;
497+
498+ expect ( sendPageViewEventSpy ) . not . toHaveBeenCalled ( ) ;
499+ expect ( sendStatsSpy ) . toHaveBeenCalled ( ) ;
500+ } ) ;
501+
502+ it ( 'should report error if counting paid impression fails' , async ( ) => {
503+ await contentpass . authenticate ( ) ;
504+
505+ const error = new Error ( 'Send page view event error' ) ;
506+ sendPageViewEventSpy . mockRejectedValue ( error ) ;
507+
508+ await contentpass . countImpression ( ) ;
509+
510+ expect ( reportErrorSpy ) . toHaveBeenCalledWith ( error , {
511+ msg : 'Failed to count paid impression' ,
512+ } ) ;
513+ expect ( sendStatsSpy ) . toHaveBeenCalled ( ) ;
514+ } ) ;
515+
516+ it ( 'should count sampled impression even if user does not have valid subscription' , async ( ) => {
517+ await contentpass . countImpression ( ) ;
518+
519+ expect ( sendPageViewEventSpy ) . not . toHaveBeenCalled ( ) ;
520+ expect ( sendStatsSpy ) . toHaveBeenCalledWith ( config . apiUrl , {
521+ cpabid : expect . any ( String ) ,
522+ cppid : '5803179c' ,
523+ cpsr : config . samplingRate ,
524+ ea : 'load' ,
525+ ec : 'tcf-sampled' ,
526+ } ) ;
527+ } ) ;
528+
529+ it ( 'should report error if counting sampled impression fails' , async ( ) => {
530+ const error = new Error ( 'Send stats error' ) ;
531+ sendStatsSpy . mockRejectedValue ( error ) ;
532+
533+ await contentpass . countImpression ( ) ;
534+
535+ expect ( reportErrorSpy ) . toHaveBeenCalledWith ( error , {
536+ msg : 'Failed to count sampled impression' ,
537+ } ) ;
538+ } ) ;
539+
540+ it ( 'should not send sampled impression when generatedSample is higher than samplingRate' , async ( ) => {
541+ jest . spyOn ( Math , 'random' ) . mockReturnValue ( 0.5 ) ;
542+ contentpass = new Contentpass ( { ...config , samplingRate : 0.4 } ) ;
543+
544+ await contentpass . countImpression ( ) ;
545+ expect ( sendStatsSpy ) . not . toHaveBeenCalled ( ) ;
546+ expect ( sendPageViewEventSpy ) . not . toHaveBeenCalled ( ) ;
547+ } ) ;
548+ } ) ;
439549} ) ;
0 commit comments