1717
1818import { expect } from 'chai' ;
1919import '../test/setup' ;
20- import { HeartbeatServiceImpl } from './heartbeatService' ;
20+ import {
21+ countBytes ,
22+ HeartbeatServiceImpl ,
23+ extractHeartbeatsForHeader
24+ } from './heartbeatService' ;
2125import {
2226 Component ,
2327 ComponentType ,
@@ -28,7 +32,7 @@ import { FirebaseApp } from './public-types';
2832import * as firebaseUtil from '@firebase/util' ;
2933import { SinonStub , stub , useFakeTimers } from 'sinon' ;
3034import * as indexedDb from './indexeddb' ;
31- import { isIndexedDBAvailable } from '@firebase/util' ;
35+ import { base64Encode , isIndexedDBAvailable } from '@firebase/util' ;
3236
3337declare module '@firebase/component' {
3438 interface NameServiceMapping {
@@ -39,6 +43,24 @@ declare module '@firebase/component' {
3943const USER_AGENT_STRING_1 = 'vs1/1.2.3 vs2/2.3.4' ;
4044const USER_AGENT_STRING_2 = 'different/1.2.3' ;
4145
46+ function generateUserAgentString ( pairs : number ) : string {
47+ let uaString = '' ;
48+ for ( let i = 0 ; i < pairs ; i ++ ) {
49+ uaString += `test-platform/${ i % 10 } .${ i % 10 } .${ i % 10 } ` ;
50+ }
51+ return uaString ;
52+ }
53+
54+ function generateDates ( count : number ) : string [ ] {
55+ let currentTimestamp = Date . now ( ) ;
56+ const dates = [ ] ;
57+ for ( let i = 0 ; i < count ; i ++ ) {
58+ dates . push ( new Date ( currentTimestamp ) . toISOString ( ) . slice ( 0 , 10 ) ) ;
59+ currentTimestamp += 24 * 60 * 60 * 1000 ;
60+ }
61+ return dates ;
62+ }
63+
4264describe ( 'HeartbeatServiceImpl' , ( ) => {
4365 describe ( 'If IndexedDB has no entries' , ( ) => {
4466 let heartbeatService : HeartbeatServiceImpl ;
@@ -80,29 +102,32 @@ describe('HeartbeatServiceImpl', () => {
80102 expect ( heartbeatService . _heartbeatsCache ?. length ) . to . equal ( 1 ) ;
81103 const heartbeat1 = heartbeatService . _heartbeatsCache ?. [ 0 ] ;
82104 expect ( heartbeat1 ?. userAgent ) . to . equal ( USER_AGENT_STRING_1 ) ;
83- expect ( heartbeat1 ?. dates [ 0 ] ) . to . equal ( '1970-01-01' ) ;
105+ expect ( heartbeat1 ?. date ) . to . equal ( '1970-01-01' ) ;
84106 expect ( writeStub ) . to . be . calledWith ( [ heartbeat1 ] ) ;
85107 } ) ;
86108 it ( `triggerHeartbeat() doesn't store another heartbeat on the same day` , async ( ) => {
109+ expect ( heartbeatService . _heartbeatsCache ?. length ) . to . equal ( 1 ) ;
87110 await heartbeatService . triggerHeartbeat ( ) ;
88- const heartbeat1 = heartbeatService . _heartbeatsCache ?. [ 0 ] ;
89- expect ( heartbeat1 ?. dates . length ) . to . equal ( 1 ) ;
111+ expect ( heartbeatService . _heartbeatsCache ?. length ) . to . equal ( 1 ) ;
90112 } ) ;
91113 it ( `triggerHeartbeat() does store another heartbeat on a different day` , async ( ) => {
114+ expect ( heartbeatService . _heartbeatsCache ?. length ) . to . equal ( 1 ) ;
92115 clock . tick ( 24 * 60 * 60 * 1000 ) ;
93116 await heartbeatService . triggerHeartbeat ( ) ;
94- const heartbeat1 = heartbeatService . _heartbeatsCache ?. [ 0 ] ;
95- expect ( heartbeat1 ?. dates . length ) . to . equal ( 2 ) ;
96- expect ( heartbeat1 ?. dates [ 1 ] ) . to . equal ( '1970-01-02' ) ;
117+ expect ( heartbeatService . _heartbeatsCache ?. length ) . to . equal ( 2 ) ;
118+ expect ( heartbeatService . _heartbeatsCache ?. [ 1 ] . date ) . to . equal (
119+ '1970-01-02'
120+ ) ;
97121 } ) ;
98122 it ( `triggerHeartbeat() stores another entry for a different user agent` , async ( ) => {
99123 userAgentString = USER_AGENT_STRING_2 ;
124+ expect ( heartbeatService . _heartbeatsCache ?. length ) . to . equal ( 2 ) ;
100125 clock . tick ( 2 * 24 * 60 * 60 * 1000 ) ;
101126 await heartbeatService . triggerHeartbeat ( ) ;
102- expect ( heartbeatService . _heartbeatsCache ?. length ) . to . equal ( 2 ) ;
103- const heartbeat2 = heartbeatService . _heartbeatsCache ?. [ 1 ] ;
104- expect ( heartbeat2 ?. dates . length ) . to . equal ( 1 ) ;
105- expect ( heartbeat2 ?. dates [ 0 ] ) . to . equal ( '1970-01-03' ) ;
127+ expect ( heartbeatService . _heartbeatsCache ?. length ) . to . equal ( 3 ) ;
128+ expect ( heartbeatService . _heartbeatsCache ?. [ 2 ] . date ) . to . equal (
129+ '1970-01-03'
130+ ) ;
106131 } ) ;
107132 it ( 'getHeartbeatHeaders() gets stored heartbeats and clears heartbeats' , async ( ) => {
108133 const deleteStub = stub ( heartbeatService . _storage , 'deleteAll' ) ;
@@ -127,9 +152,14 @@ describe('HeartbeatServiceImpl', () => {
127152 let writeStub : SinonStub ;
128153 let userAgentString = USER_AGENT_STRING_1 ;
129154 const mockIndexedDBHeartbeats = [
155+ // Chosen so one will exceed 30 day limit and one will not.
130156 {
131157 userAgent : 'old-user-agent' ,
132- dates : [ '1969-01-01' , '1969-01-02' ]
158+ date : '1969-12-01'
159+ } ,
160+ {
161+ userAgent : 'old-user-agent' ,
162+ date : '1969-12-31'
133163 }
134164 ] ;
135165 before ( ( ) => {
@@ -179,18 +209,19 @@ describe('HeartbeatServiceImpl', () => {
179209 expect ( heartbeatService . _heartbeatsCache ) . to . deep . equal ( [ ] ) ;
180210 }
181211 } ) ;
182- it ( `triggerHeartbeat() writes new heartbeats without removing old ones` , async ( ) => {
212+ it ( `triggerHeartbeat() writes new heartbeats and retains old ones newer than 30 days ` , async ( ) => {
183213 userAgentString = USER_AGENT_STRING_2 ;
184214 clock . tick ( 3 * 24 * 60 * 60 * 1000 ) ;
185215 await heartbeatService . triggerHeartbeat ( ) ;
186216 if ( isIndexedDBAvailable ( ) ) {
187217 expect ( writeStub ) . to . be . calledWith ( [
188- ...mockIndexedDBHeartbeats ,
189- { userAgent : USER_AGENT_STRING_2 , dates : [ '1970-01-04' ] }
218+ // The first entry exceeds the 30 day retention limit.
219+ mockIndexedDBHeartbeats [ 1 ] ,
220+ { userAgent : USER_AGENT_STRING_2 , date : '1970-01-04' }
190221 ] ) ;
191222 } else {
192223 expect ( writeStub ) . to . be . calledWith ( [
193- { userAgent : USER_AGENT_STRING_2 , dates : [ '1970-01-04' ] }
224+ { userAgent : USER_AGENT_STRING_2 , date : '1970-01-04' }
194225 ] ) ;
195226 }
196227 } ) ;
@@ -201,8 +232,7 @@ describe('HeartbeatServiceImpl', () => {
201232 ) ;
202233 if ( isIndexedDBAvailable ( ) ) {
203234 expect ( heartbeatHeaders ) . to . include ( 'old-user-agent' ) ;
204- expect ( heartbeatHeaders ) . to . include ( '1969-01-01' ) ;
205- expect ( heartbeatHeaders ) . to . include ( '1969-01-02' ) ;
235+ expect ( heartbeatHeaders ) . to . include ( '1969-12-31' ) ;
206236 }
207237 expect ( heartbeatHeaders ) . to . include ( USER_AGENT_STRING_2 ) ;
208238 expect ( heartbeatHeaders ) . to . include ( '1970-01-04' ) ;
@@ -213,4 +243,78 @@ describe('HeartbeatServiceImpl', () => {
213243 expect ( deleteStub ) . to . be . called ;
214244 } ) ;
215245 } ) ;
246+
247+ describe ( 'countBytes()' , ( ) => {
248+ it ( 'counts how many bytes there will be in a stringified, encoded header' , ( ) => {
249+ const heartbeats = [
250+ { userAgent : generateUserAgentString ( 1 ) , dates : generateDates ( 1 ) } ,
251+ { userAgent : generateUserAgentString ( 3 ) , dates : generateDates ( 2 ) }
252+ ] ;
253+ let size : number = 0 ;
254+ const headerString = base64Encode (
255+ JSON . stringify ( { version : 2 , heartbeats } )
256+ ) ;
257+ // Use independent methods to validate our byte count method matches.
258+ // We don't use this measurement method in the app because user
259+ // environments are much more unpredictable while we know the
260+ // tests will run in either a standard headless browser or Node.
261+ if ( typeof Blob !== 'undefined' ) {
262+ const blob = new Blob ( [ headerString ] ) ;
263+ size = blob . size ;
264+ } else if ( typeof Buffer !== 'undefined' ) {
265+ const buffer = Buffer . from ( headerString ) ;
266+ size = buffer . byteLength ;
267+ }
268+ expect ( countBytes ( heartbeats ) ) . to . equal ( size ) ;
269+ } ) ;
270+ } ) ;
271+
272+ describe ( '_extractHeartbeatsForHeader()' , ( ) => {
273+ it ( 'returns empty heartbeatsToKeep if it cannot get under maxSize' , ( ) => {
274+ const heartbeats = [
275+ { userAgent : generateUserAgentString ( 1 ) , date : '2022-01-01' }
276+ ] ;
277+ const { unsentEntries, heartbeatsToSend } = extractHeartbeatsForHeader (
278+ heartbeats ,
279+ 5
280+ ) ;
281+ expect ( heartbeatsToSend . length ) . to . equal ( 0 ) ;
282+ expect ( unsentEntries ) . to . deep . equal ( heartbeats ) ;
283+ } ) ;
284+ it ( 'splits heartbeats array' , ( ) => {
285+ const heartbeats = [
286+ { userAgent : generateUserAgentString ( 20 ) , date : '2022-01-01' } ,
287+ { userAgent : generateUserAgentString ( 4 ) , date : '2022-01-02' }
288+ ] ;
289+ const sizeWithHeartbeat0Only = countBytes ( [
290+ { userAgent : heartbeats [ 0 ] . userAgent , dates : [ heartbeats [ 0 ] . date ] }
291+ ] ) ;
292+ const { unsentEntries, heartbeatsToSend } = extractHeartbeatsForHeader (
293+ heartbeats ,
294+ sizeWithHeartbeat0Only + 1
295+ ) ;
296+ expect ( heartbeatsToSend . length ) . to . equal ( 1 ) ;
297+ expect ( unsentEntries . length ) . to . equal ( 1 ) ;
298+ } ) ;
299+ it ( 'splits the first heartbeat if needed' , ( ) => {
300+ const uaString = generateUserAgentString ( 20 ) ;
301+ const heartbeats = [
302+ { userAgent : uaString , date : '2022-01-01' } ,
303+ { userAgent : uaString , date : '2022-01-02' } ,
304+ { userAgent : uaString , date : '2022-01-03' }
305+ ] ;
306+ const sizeWithHeartbeat0Only = countBytes ( [
307+ { userAgent : heartbeats [ 0 ] . userAgent , dates : [ heartbeats [ 0 ] . date ] }
308+ ] ) ;
309+ const { unsentEntries, heartbeatsToSend } = extractHeartbeatsForHeader (
310+ heartbeats ,
311+ sizeWithHeartbeat0Only + 1
312+ ) ;
313+ expect ( heartbeatsToSend . length ) . to . equal ( 1 ) ;
314+ expect ( unsentEntries . length ) . to . equal ( 2 ) ;
315+ expect ( heartbeatsToSend [ 0 ] . dates . length + unsentEntries . length ) . to . equal (
316+ heartbeats . length
317+ ) ;
318+ } ) ;
319+ } ) ;
216320} ) ;
0 commit comments