17
17
18
18
import { expect } from 'chai' ;
19
19
import '../test/setup' ;
20
- import { HeartbeatServiceImpl } from './heartbeatService' ;
20
+ import {
21
+ countBytes ,
22
+ HeartbeatServiceImpl ,
23
+ extractHeartbeatsForHeader
24
+ } from './heartbeatService' ;
21
25
import {
22
26
Component ,
23
27
ComponentType ,
@@ -28,7 +32,7 @@ import { FirebaseApp } from './public-types';
28
32
import * as firebaseUtil from '@firebase/util' ;
29
33
import { SinonStub , stub , useFakeTimers } from 'sinon' ;
30
34
import * as indexedDb from './indexeddb' ;
31
- import { isIndexedDBAvailable } from '@firebase/util' ;
35
+ import { base64Encode , isIndexedDBAvailable } from '@firebase/util' ;
32
36
33
37
declare module '@firebase/component' {
34
38
interface NameServiceMapping {
@@ -39,6 +43,24 @@ declare module '@firebase/component' {
39
43
const USER_AGENT_STRING_1 = 'vs1/1.2.3 vs2/2.3.4' ;
40
44
const USER_AGENT_STRING_2 = 'different/1.2.3' ;
41
45
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
+
42
64
describe ( 'HeartbeatServiceImpl' , ( ) => {
43
65
describe ( 'If IndexedDB has no entries' , ( ) => {
44
66
let heartbeatService : HeartbeatServiceImpl ;
@@ -80,29 +102,32 @@ describe('HeartbeatServiceImpl', () => {
80
102
expect ( heartbeatService . _heartbeatsCache ?. length ) . to . equal ( 1 ) ;
81
103
const heartbeat1 = heartbeatService . _heartbeatsCache ?. [ 0 ] ;
82
104
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' ) ;
84
106
expect ( writeStub ) . to . be . calledWith ( [ heartbeat1 ] ) ;
85
107
} ) ;
86
108
it ( `triggerHeartbeat() doesn't store another heartbeat on the same day` , async ( ) => {
109
+ expect ( heartbeatService . _heartbeatsCache ?. length ) . to . equal ( 1 ) ;
87
110
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 ) ;
90
112
} ) ;
91
113
it ( `triggerHeartbeat() does store another heartbeat on a different day` , async ( ) => {
114
+ expect ( heartbeatService . _heartbeatsCache ?. length ) . to . equal ( 1 ) ;
92
115
clock . tick ( 24 * 60 * 60 * 1000 ) ;
93
116
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
+ ) ;
97
121
} ) ;
98
122
it ( `triggerHeartbeat() stores another entry for a different user agent` , async ( ) => {
99
123
userAgentString = USER_AGENT_STRING_2 ;
124
+ expect ( heartbeatService . _heartbeatsCache ?. length ) . to . equal ( 2 ) ;
100
125
clock . tick ( 2 * 24 * 60 * 60 * 1000 ) ;
101
126
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
+ ) ;
106
131
} ) ;
107
132
it ( 'getHeartbeatHeaders() gets stored heartbeats and clears heartbeats' , async ( ) => {
108
133
const deleteStub = stub ( heartbeatService . _storage , 'deleteAll' ) ;
@@ -127,9 +152,14 @@ describe('HeartbeatServiceImpl', () => {
127
152
let writeStub : SinonStub ;
128
153
let userAgentString = USER_AGENT_STRING_1 ;
129
154
const mockIndexedDBHeartbeats = [
155
+ // Chosen so one will exceed 30 day limit and one will not.
130
156
{
131
157
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'
133
163
}
134
164
] ;
135
165
before ( ( ) => {
@@ -179,18 +209,19 @@ describe('HeartbeatServiceImpl', () => {
179
209
expect ( heartbeatService . _heartbeatsCache ) . to . deep . equal ( [ ] ) ;
180
210
}
181
211
} ) ;
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 ( ) => {
183
213
userAgentString = USER_AGENT_STRING_2 ;
184
214
clock . tick ( 3 * 24 * 60 * 60 * 1000 ) ;
185
215
await heartbeatService . triggerHeartbeat ( ) ;
186
216
if ( isIndexedDBAvailable ( ) ) {
187
217
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' }
190
221
] ) ;
191
222
} else {
192
223
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' }
194
225
] ) ;
195
226
}
196
227
} ) ;
@@ -201,8 +232,7 @@ describe('HeartbeatServiceImpl', () => {
201
232
) ;
202
233
if ( isIndexedDBAvailable ( ) ) {
203
234
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' ) ;
206
236
}
207
237
expect ( heartbeatHeaders ) . to . include ( USER_AGENT_STRING_2 ) ;
208
238
expect ( heartbeatHeaders ) . to . include ( '1970-01-04' ) ;
@@ -213,4 +243,78 @@ describe('HeartbeatServiceImpl', () => {
213
243
expect ( deleteStub ) . to . be . called ;
214
244
} ) ;
215
245
} ) ;
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
+ } ) ;
216
320
} ) ;
0 commit comments