@@ -5,15 +5,19 @@ 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 planId : 'planId-1' ,
1517 redirectUrl : 'de.test.net://oauth' ,
1618 issuer : 'https://issuer.net' ,
19+ apiUrl : 'https://cp.property.com' ,
20+ samplingRate : 1 ,
1721} ;
1822
1923const NOW = new Date ( '2024-12-02T11:53:56.272Z' ) . getTime ( ) ;
@@ -37,13 +41,21 @@ const EXAMPLE_REFRESH_RESULT = {
3741 accessTokenExpirationDate : '2024-12-03T10:00:50Z' ,
3842} ;
3943
44+ const CONTENTPASS_TOKEN_WITH_PLANS =
45+ 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjY5NzUwYTZjLTNmYjctNDUyNi05NWY4LTVhZmYxMmIyZjFjOSJ9.eyJhdXRoIjp0cnVlLCJ0eXBlIjoiY3AiLCJwbGFucyI6WyIwYWNhZTkxNy1iZTk5LTQ4ZWEtYjhmMS0yMGZhNjhhNDdkM2EiLCI0NDIxNjI4Yy05NjA2LTRjMDEtOGU1ZC1jMmE5YmNhNjhhYjQiLCI3ZThkZTBjYy0zZTk3LTQ5YTItODgxZC05ZmZiNWI4NDE1MTUiLCJhNDcyMWRiNS02N2RmLTQxNDUtYmJiZi1jYmQwOWY3ZTAzOTciLCJjNGQzYjBmNS05ODlhLTRmN2ItOGFjNy0zZDhmZmE5NTcxN2YiLCI2NGRkOTkwNS05NmUxLTRmYjItOTgwZC01MDdmMTYzNzVmZTkiXSwiYXVkIjoiY2MzZmM0YWQiLCJpYXQiOjE3MzMxMzU2ODEsImV4cCI6MTczMzMxMjA4MX0.CMtH7HRLf2HVgw3_cZRN0en8tml_SQKM73iLGJAp72-vJuRJaq85xBp6Jgy9WD3L7x4itRlBAYZxX8tLxZGogU0WP4_dMGFQ2QlcwKshwJygwRM1YqvxGWX2Az_KxEMc2QGHvpE1qe2MAr_xOU7VFfc0-vWxFc3hRzpAM5j7YHctj2t1v6h9-M7V2Hkcn37569QmtgU8gJkUxXsgUTufbb1ikjjjAvnjvTluHJo51_utbimpUbCk3EFxXVCVEI_pAqiZQXNninUQ6dbSujLb3L2UlEdQzLeUiBdYroeFzSyruLrR841ledLQ5ZP2OqzF5oUMuAGVOOhmgGdwGMCDRQ' ;
46+
47+ export const CONTENTPASS_TOKEN_WITHOUT_PLANS =
48+ 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjY5NzUwYTZjLTNmYjctNDUyNi05NWY4LTVhZmYxMmIyZjFjOSJ9.eyJhdXRoIjp0cnVlLCJ0eXBlIjoiY3AiLCJwbGFucyI6W10sImF1ZCI6ImNjM2ZjNGFkIiwiaWF0IjoxNzMzMTM1NjgxLCJleHAiOjE3MzMzMTIwODF9.CMtH7HRLf2HVgw3_cZRN0en8tml_SQKM73iLGJAp72-vJuRJaq85xBp6Jgy9WD3L7x4itRlBAYZxX8tLxZGogU0WP4_dMGFQ2QlcwKshwJygwRM1YqvxGWX2Az_KxEMc2QGHvpE1qe2MAr_xOU7VFfc0-vWxFc3hRzpAM5j7YHctj2t1v6h9-M7V2Hkcn37569QmtgU8gJkUxXsgUTufbb1ikjjjAvnjvTluHJo51_utbimpUbCk3EFxXVCVEI_pAqiZQXNninUQ6dbSujLb3L2UlEdQzLeUiBdYroeFzSyruLrR841ledLQ5ZP2OqzF5oUMuAGVOOhmgGdwGMCDRQ' ;
49+
4050describe ( 'Contentpass' , ( ) => {
4151 let contentpass : Contentpass ;
4252 let authorizeSpy : jest . SpyInstance ;
4353 let refreshSpy : jest . SpyInstance ;
4454 let reportErrorSpy : jest . SpyInstance ;
4555 let fetchContentpassTokenSpy : jest . SpyInstance ;
4656 let oidcAuthStorageMock : OidcAuthStateStorage ;
57+ let sendStatsSpy : jest . SpyInstance ;
58+ let sendPageViewEventSpy : jest . SpyInstance ;
4759
4860 beforeEach ( ( ) => {
4961 jest . useFakeTimers ( { now : NOW } ) ;
@@ -69,9 +81,15 @@ describe('Contentpass', () => {
6981
7082 fetchContentpassTokenSpy = jest
7183 . spyOn ( FetchContentpassTokenModule , 'default' )
72- . mockResolvedValue (
73- 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjY5NzUwYTZjLTNmYjctNDUyNi05NWY4LTVhZmYxMmIyZjFjOSJ9.eyJhdXRoIjp0cnVlLCJ0eXBlIjoiY3AiLCJwbGFucyI6WyIwYWNhZTkxNy1iZTk5LTQ4ZWEtYjhmMS0yMGZhNjhhNDdkM2EiLCI0NDIxNjI4Yy05NjA2LTRjMDEtOGU1ZC1jMmE5YmNhNjhhYjQiLCI3ZThkZTBjYy0zZTk3LTQ5YTItODgxZC05ZmZiNWI4NDE1MTUiLCJhNDcyMWRiNS02N2RmLTQxNDUtYmJiZi1jYmQwOWY3ZTAzOTciLCJjNGQzYjBmNS05ODlhLTRmN2ItOGFjNy0zZDhmZmE5NTcxN2YiLCI2NGRkOTkwNS05NmUxLTRmYjItOTgwZC01MDdmMTYzNzVmZTkiXSwiYXVkIjoiY2MzZmM0YWQiLCJpYXQiOjE3MzMxMzU2ODEsImV4cCI6MTczMzMxMjA4MX0.CMtH7HRLf2HVgw3_cZRN0en8tml_SQKM73iLGJAp72-vJuRJaq85xBp6Jgy9WD3L7x4itRlBAYZxX8tLxZGogU0WP4_dMGFQ2QlcwKshwJygwRM1YqvxGWX2Az_KxEMc2QGHvpE1qe2MAr_xOU7VFfc0-vWxFc3hRzpAM5j7YHctj2t1v6h9-M7V2Hkcn37569QmtgU8gJkUxXsgUTufbb1ikjjjAvnjvTluHJo51_utbimpUbCk3EFxXVCVEI_pAqiZQXNninUQ6dbSujLb3L2UlEdQzLeUiBdYroeFzSyruLrR841ledLQ5ZP2OqzF5oUMuAGVOOhmgGdwGMCDRQ'
74- ) ;
84+ . mockResolvedValue ( CONTENTPASS_TOKEN_WITH_PLANS ) ;
85+
86+ sendStatsSpy = jest
87+ . spyOn ( SendStatsModule , 'default' )
88+ . mockResolvedValue ( { ok : true } as any ) ;
89+
90+ sendPageViewEventSpy = jest
91+ . spyOn ( SendPageViewEventModule , 'default' )
92+ . mockResolvedValue ( { ok : true } as any ) ;
7593
7694 contentpass = new Contentpass ( config ) ;
7795 } ) ;
@@ -83,6 +101,24 @@ describe('Contentpass', () => {
83101 } ) ;
84102
85103 describe ( 'constructor' , ( ) => {
104+ it ( 'should throw an error if sampling rate is not between 0 and 1' , ( ) => {
105+ expect (
106+ ( ) =>
107+ new Contentpass ( {
108+ ...config ,
109+ samplingRate : - 1 ,
110+ } )
111+ ) . toThrow ( 'Sampling rate must be between 0 and 1' ) ;
112+
113+ expect (
114+ ( ) =>
115+ new Contentpass ( {
116+ ...config ,
117+ samplingRate : 2 ,
118+ } )
119+ ) . toThrow ( 'Sampling rate must be between 0 and 1' ) ;
120+ } ) ;
121+
86122 it ( 'should initialise contentpass state' , ( ) => {
87123 const contentpassStates : ContentpassState [ ] = [ ] ;
88124 contentpass . registerObserver ( ( state ) => {
@@ -138,9 +174,9 @@ describe('Contentpass', () => {
138174 expect ( refreshSpy ) . toHaveBeenCalledTimes ( 1 ) ;
139175 expect ( refreshSpy ) . toHaveBeenCalledWith (
140176 {
141- clientId : ' propertyId-1' ,
142- redirectUrl : 'de.test.net://oauth' ,
143- issuer : 'https://issuer.net' ,
177+ clientId : config . propertyId ,
178+ redirectUrl : config . redirectUrl ,
179+ issuer : config . issuer ,
144180 scopes : SCOPES ,
145181 } ,
146182 { refreshToken : EXAMPLE_AUTH_RESULT . refreshToken }
@@ -159,14 +195,14 @@ describe('Contentpass', () => {
159195
160196 expect ( authorizeSpy ) . toHaveBeenCalledWith ( {
161197 additionalParameters : {
162- cp_property : ' propertyId-1' ,
163- cp_plan : ' planId-1' ,
198+ cp_property : config . propertyId ,
199+ cp_plan : config . planId ,
164200 cp_route : 'login' ,
165201 prompt : 'consent' ,
166202 } ,
167- clientId : ' propertyId-1' ,
168- issuer : 'https://issuer.net' ,
169- redirectUrl : 'de.test.net://oauth' ,
203+ clientId : config . propertyId ,
204+ issuer : config . issuer ,
205+ redirectUrl : config . redirectUrl ,
170206 scopes : [ 'openid' , 'offline_access' , 'contentpass' ] ,
171207 } ) ;
172208 } ) ;
@@ -250,9 +286,9 @@ describe('Contentpass', () => {
250286 expect ( refreshSpy ) . toHaveBeenCalledTimes ( 1 ) ;
251287 expect ( refreshSpy ) . toHaveBeenCalledWith (
252288 {
253- clientId : ' propertyId-1' ,
254- redirectUrl : 'de.test.net://oauth' ,
255- issuer : 'https://issuer.net' ,
289+ clientId : config . propertyId ,
290+ redirectUrl : config . redirectUrl ,
291+ issuer : config . issuer ,
256292 scopes : SCOPES ,
257293 } ,
258294 { refreshToken : EXAMPLE_AUTH_RESULT . refreshToken }
@@ -438,4 +474,78 @@ describe('Contentpass', () => {
438474 } ) ;
439475 } ) ;
440476 } ) ;
477+
478+ describe ( 'countImpression' , ( ) => {
479+ it ( 'should count paid impression if user has valid subscription and access token' , async ( ) => {
480+ await contentpass . authenticate ( ) ;
481+
482+ await contentpass . countImpression ( ) ;
483+
484+ expect ( sendPageViewEventSpy ) . toHaveBeenCalledWith ( config . apiUrl , {
485+ propertyId : config . propertyId ,
486+ impressionId : expect . any ( String ) ,
487+ accessToken : EXAMPLE_AUTH_RESULT . accessToken ,
488+ } ) ;
489+ expect ( sendStatsSpy ) . toHaveBeenCalled ( ) ;
490+ } ) ;
491+
492+ it ( 'should not count paid impression if user is authenticated, but does not have valid subscription' , async ( ) => {
493+ fetchContentpassTokenSpy . mockResolvedValue (
494+ CONTENTPASS_TOKEN_WITHOUT_PLANS
495+ ) ;
496+ await contentpass . authenticate ( ) ;
497+
498+ await contentpass . countImpression ( ) ;
499+
500+ expect ( sendPageViewEventSpy ) . not . toHaveBeenCalled ( ) ;
501+ expect ( sendStatsSpy ) . toHaveBeenCalled ( ) ;
502+ } ) ;
503+
504+ it ( 'should report error if counting paid impression fails' , async ( ) => {
505+ await contentpass . authenticate ( ) ;
506+
507+ const error = new Error ( 'Send page view event error' ) ;
508+ sendPageViewEventSpy . mockRejectedValue ( error ) ;
509+
510+ await contentpass . countImpression ( ) ;
511+
512+ expect ( reportErrorSpy ) . toHaveBeenCalledWith ( error , {
513+ msg : 'Failed to count paid impression' ,
514+ } ) ;
515+ expect ( sendStatsSpy ) . toHaveBeenCalled ( ) ;
516+ } ) ;
517+
518+ it ( 'should count sampled impression even if user does not have valid subscription' , async ( ) => {
519+ await contentpass . countImpression ( ) ;
520+
521+ expect ( sendPageViewEventSpy ) . not . toHaveBeenCalled ( ) ;
522+ expect ( sendStatsSpy ) . toHaveBeenCalledWith ( config . apiUrl , {
523+ cpabid : expect . any ( String ) ,
524+ cppid : '5803179c' ,
525+ cpsr : config . samplingRate ,
526+ ea : 'load' ,
527+ ec : 'tcf-sampled' ,
528+ } ) ;
529+ } ) ;
530+
531+ it ( 'should report error if counting sampled impression fails' , async ( ) => {
532+ const error = new Error ( 'Send stats error' ) ;
533+ sendStatsSpy . mockRejectedValue ( error ) ;
534+
535+ await contentpass . countImpression ( ) ;
536+
537+ expect ( reportErrorSpy ) . toHaveBeenCalledWith ( error , {
538+ msg : 'Failed to count sampled impression' ,
539+ } ) ;
540+ } ) ;
541+
542+ it ( 'should not send sampled impression when generatedSample is higher than samplingRate' , async ( ) => {
543+ jest . spyOn ( Math , 'random' ) . mockReturnValue ( 0.5 ) ;
544+ contentpass = new Contentpass ( { ...config , samplingRate : 0.4 } ) ;
545+
546+ await contentpass . countImpression ( ) ;
547+ expect ( sendStatsSpy ) . not . toHaveBeenCalled ( ) ;
548+ expect ( sendPageViewEventSpy ) . not . toHaveBeenCalled ( ) ;
549+ } ) ;
550+ } ) ;
441551} ) ;
0 commit comments