Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/core/test/user-profile.test.ts
1029 views
1
import { Helpers } from '@secret-agent/testing';
2
import { InteractionCommand } from '@secret-agent/interfaces/IInteractions';
3
import IUserProfile from '@secret-agent/interfaces/IUserProfile';
4
import HttpRequestHandler from '@secret-agent/mitm/handlers/HttpRequestHandler';
5
import { ITestKoaServer } from '@secret-agent/testing/helpers';
6
import { createPromise } from '@secret-agent/commons/utils';
7
import MitmRequestAgent from '@secret-agent/mitm/lib/MitmRequestAgent';
8
import IDomStorage from '@secret-agent/interfaces/IDomStorage';
9
import Core from '../index';
10
import ConnectionToClient from '../server/ConnectionToClient';
11
import Session from '../lib/Session';
12
13
let koaServer: ITestKoaServer;
14
let connection: ConnectionToClient;
15
beforeAll(async () => {
16
connection = Core.addConnection();
17
await connection.connect();
18
Helpers.onClose(() => connection.disconnect(), true);
19
koaServer = await Helpers.runKoaServer();
20
});
21
afterAll(Helpers.afterAll);
22
afterEach(Helpers.afterEach);
23
24
describe('UserProfile cookie tests', () => {
25
it('should be able to save and restore cookies', async () => {
26
const meta = await connection.createSession();
27
const tab = Session.getTab(meta);
28
Helpers.needsClosing.push(tab.session);
29
30
koaServer.get('/cookie', ctx => {
31
ctx.cookies.set('cookietest', 'Is Set');
32
ctx.body = `<body><h1>cookie page</h1></body>`;
33
});
34
35
let cookie = 'not set';
36
koaServer.get('/cookie2', ctx => {
37
cookie = ctx.cookies.get('cookietest');
38
ctx.body = `<body><h1>cookie page 2</h1></body>`;
39
});
40
41
await tab.goto(`${koaServer.baseUrl}/cookie`);
42
await tab.waitForLoad('PaintingStable');
43
44
const profile = await connection.exportUserProfile(meta);
45
expect(profile.cookies).toHaveLength(1);
46
expect(profile.cookies[0].name).toBe('cookietest');
47
expect(profile.cookies[0].value).toBe('Is Set');
48
49
// try loading an empty session now to confirm cookies are gone without reloading
50
const meta2 = await connection.createSession();
51
const tab2 = Session.getTab(meta2);
52
Helpers.needsClosing.push(tab2.session);
53
const core2Cookies = await connection.exportUserProfile(meta2);
54
expect(core2Cookies.cookies).toHaveLength(0);
55
56
await tab2.goto(`${koaServer.baseUrl}/cookie2`);
57
await tab2.waitForLoad('PaintingStable');
58
expect(cookie).not.toBeTruthy();
59
60
expect(profile.userAgentString).toBeTruthy();
61
62
const meta3 = await connection.createSession({
63
userProfile: profile,
64
});
65
const tab3 = Session.getTab(meta3);
66
Helpers.needsClosing.push(tab3.session);
67
const cookiesBefore = await connection.exportUserProfile(meta3);
68
expect(cookiesBefore.cookies).toHaveLength(1);
69
expect(cookiesBefore.userAgentString).toBe(profile.userAgentString);
70
expect(tab3.session.plugins.browserEmulator.userAgentString).toBe(profile.userAgentString);
71
expect(cookiesBefore.deviceProfile).toEqual(profile.deviceProfile);
72
73
await tab3.goto(`${koaServer.baseUrl}/cookie2`);
74
await tab3.waitForLoad('PaintingStable');
75
expect(cookie).toBe('Is Set');
76
});
77
78
it('should track cookies from other domains', async () => {
79
let profile: IUserProfile;
80
{
81
const meta = await connection.createSession();
82
const tab = Session.getTab(meta);
83
const session = tab.session;
84
Helpers.needsClosing.push(session);
85
session.mitmRequestSession.blockedResources = {
86
urls: ['https://dataliberationfoundation.org/*'],
87
types: [],
88
handlerFn(request, response) {
89
response.setHeader('Set-Cookie', [
90
'cross1=1; SameSite=None; Secure; HttpOnly',
91
'cross2=2; SameSite=None; Secure; HttpOnly',
92
]);
93
response.end(`<html><p>frame body</p></html>`);
94
return true;
95
},
96
};
97
koaServer.get('/cross-cookie', ctx => {
98
ctx.cookies.set('cookietest', 'mainsite');
99
ctx.body = `<body><h1>cross cookies page</h1><iframe src="https://dataliberationfoundation.org/cookie"/></body>`;
100
});
101
102
await tab.goto(`${koaServer.baseUrl}/cross-cookie`);
103
await tab.waitForLoad('PaintingStable');
104
await tab.puppetPage.frames[1].waitForLifecycleEvent('load');
105
106
profile = await connection.exportUserProfile(meta);
107
expect(profile.cookies).toHaveLength(3);
108
expect(profile.cookies[0].name).toBe('cookietest');
109
expect(profile.cookies[0].value).toBe('mainsite');
110
expect(profile.cookies[1].name).toBe('cross1');
111
expect(profile.cookies[1].value).toBe('1');
112
await session.close();
113
}
114
{
115
const meta = await connection.createSession({
116
userProfile: profile,
117
});
118
const tab = Session.getTab(meta);
119
const session = tab.session;
120
Helpers.needsClosing.push(session);
121
122
const dlfCookies = createPromise<string>();
123
const sameCookies = createPromise<string>();
124
125
session.mitmRequestSession.blockedResources = {
126
urls: ['https://dataliberationfoundation.org/*'],
127
types: [],
128
handlerFn: (request, response) => {
129
dlfCookies.resolve(request.headers.cookie);
130
response.end(`<html><p>frame body</p></html>`);
131
return true;
132
},
133
};
134
koaServer.get('/cross-cookie2', ctx => {
135
sameCookies.resolve(ctx.cookies.get('cookietest'));
136
ctx.body = `<body><h1>cross cookies page</h1><iframe src="https://dataliberationfoundation.org/cookie2"/></body>`;
137
});
138
await tab.goto(`${koaServer.baseUrl}/cross-cookie2`);
139
await tab.waitForLoad('PaintingStable');
140
141
await expect(dlfCookies).resolves.toBe('cross1=1; cross2=2');
142
await expect(sameCookies).resolves.toBe('mainsite');
143
await session.close();
144
}
145
});
146
});
147
148
describe('UserProfile Dom storage tests', () => {
149
it('should be able to save and restore local/session storage', async () => {
150
const meta = await connection.createSession();
151
const tab = Session.getTab(meta);
152
Helpers.needsClosing.push(tab.session);
153
154
koaServer.get('/local', ctx => {
155
ctx.body = `<body>
156
<h1>storage page</h1>
157
<script>
158
localStorage.setItem('Test1', 'value1');
159
localStorage.setItem('Test2', 'value2');
160
localStorage.setItem('Test3', 'value3');
161
localStorage.removeItem('Test2');
162
163
sessionStorage.setItem('STest1', 'value1');
164
sessionStorage.setItem('STest2', 'value2');
165
sessionStorage.setItem('STest3', 'value3');
166
sessionStorage.removeItem('STest3');
167
</script>
168
</body>`;
169
});
170
171
koaServer.get('/localrestore', ctx => {
172
ctx.body = `<body>
173
<div id="local"></div>
174
<div id="session"></div>
175
<script>
176
const local1 = localStorage.getItem('Test1');
177
const local2 = localStorage.getItem('Test2');
178
const local3 = localStorage.getItem('Test3');
179
document.querySelector('#local').innerHTML = [local1,local2,local3].join(',');
180
181
const session1 = sessionStorage.getItem('STest1');
182
const session2 = sessionStorage.getItem('STest2');
183
const session3 = sessionStorage.getItem('STest3');
184
document.querySelector('#session').innerHTML = [session1,session2,session3].join(',');
185
</script>
186
</body>`;
187
});
188
189
await tab.goto(`${koaServer.baseUrl}/local`);
190
await tab.waitForLoad('PaintingStable');
191
192
const profile = await connection.exportUserProfile(meta);
193
expect(profile.cookies).toHaveLength(0);
194
expect(profile.storage[koaServer.baseUrl]?.localStorage).toHaveLength(2);
195
expect(profile.storage[koaServer.baseUrl]?.sessionStorage).toHaveLength(2);
196
197
const meta2 = await connection.createSession({
198
userProfile: profile,
199
});
200
const tab2 = Session.getTab(meta2);
201
Helpers.needsClosing.push(tab2.session);
202
203
await tab2.goto(`${koaServer.baseUrl}/localrestore`);
204
await tab2.waitForLoad('PaintingStable');
205
206
const localContent = await tab2.execJsPath([
207
'document',
208
['querySelector', '#local'],
209
'textContent',
210
]);
211
expect(localContent.value).toBe('value1,,value3');
212
const sessionContent = await tab2.execJsPath([
213
'document',
214
['querySelector', '#session'],
215
'textContent',
216
]);
217
expect(sessionContent.value).toBe('value1,value2,');
218
219
await tab.close();
220
await tab2.close();
221
});
222
223
it('should be able to restore storage for a large number of sites', async () => {
224
const storage: IDomStorage = {
225
[`http://${koaServer.baseHost}`]: {
226
indexedDB: [],
227
localStorage: [['test', '1']],
228
sessionStorage: [['test', '2']],
229
},
230
};
231
for (let i = 0; i < 100; i += 1) {
232
storage[`https://domain${i}.com`] = {
233
indexedDB: [],
234
localStorage: [
235
['1', '2'],
236
['test2', '1'],
237
],
238
sessionStorage: [
239
['1', '2'],
240
['test2', '1'],
241
],
242
};
243
}
244
const meta = await connection.createSession({
245
userProfile: {
246
storage,
247
},
248
});
249
const tab = Session.getTab(meta);
250
Helpers.needsClosing.push(tab.session);
251
252
await tab.goto(`${koaServer.baseUrl}`);
253
await tab.waitForLoad('PaintingStable');
254
await expect(tab.getJsValue('localStorage.getItem("test")')).resolves.toBe('1');
255
await expect(tab.getJsValue('sessionStorage.getItem("test")')).resolves.toBe('2');
256
});
257
258
it("should keep profile information for sites that aren't loaded in a session", async () => {
259
const meta = await connection.createSession({
260
userProfile: {
261
cookies: [],
262
storage: {
263
[koaServer.baseUrl]: {
264
indexedDB: [],
265
localStorage: [
266
['Test1', 'value0'],
267
['test2', 'value1'],
268
],
269
sessionStorage: [],
270
},
271
'https://previousSite.org': {
272
indexedDB: [],
273
localStorage: [['test', 'site1.org']],
274
sessionStorage: [],
275
},
276
'https://site2.org': {
277
indexedDB: [],
278
localStorage: [['test2', 'site2.org']],
279
sessionStorage: [],
280
},
281
},
282
},
283
});
284
const tab = Session.getTab(meta);
285
Helpers.needsClosing.push(tab.session);
286
287
koaServer.get('/unloaded', ctx => {
288
ctx.body = `<body>
289
<h1>storage page</h1>
290
<script>
291
localStorage.setItem('Test1', 'value1');
292
</script>
293
</body>`;
294
});
295
296
await tab.goto(`${koaServer.baseUrl}/unloaded`);
297
await tab.waitForLoad('PaintingStable');
298
299
const profile = await tab.session.exportUserProfile();
300
expect(profile.cookies).toHaveLength(0);
301
expect(profile.storage[koaServer.baseUrl]?.localStorage).toHaveLength(2);
302
expect(profile.storage[koaServer.baseUrl]?.localStorage.find(x => x[0] === 'Test1')).toEqual([
303
'Test1',
304
'value1',
305
]);
306
expect(profile.storage['https://previousSite.org'].localStorage).toEqual([
307
['test', 'site1.org'],
308
]);
309
expect(profile.storage['https://site2.org'].localStorage).toEqual([['test2', 'site2.org']]);
310
await tab.close();
311
});
312
313
it('should not make requests to end sites during profile "install"', async () => {
314
const mitmSpy = jest.spyOn(MitmRequestAgent.prototype, 'request');
315
await connection.createSession({
316
userProfile: {
317
cookies: [],
318
storage: {
319
'https://site1.org': {
320
indexedDB: [],
321
localStorage: [['test', 'site1.org']],
322
sessionStorage: [],
323
},
324
'https://site2.org': {
325
indexedDB: [],
326
localStorage: [['test2', 'site2.org']],
327
sessionStorage: [],
328
},
329
},
330
},
331
});
332
expect(mitmSpy).toHaveBeenCalledTimes(0);
333
});
334
335
it('should not override changed variables on a second page load in a domain', async () => {
336
const meta = await connection.createSession({
337
userProfile: {
338
storage: {
339
[koaServer.baseUrl]: {
340
indexedDB: [],
341
localStorage: [['test', 'beforeChange']],
342
sessionStorage: [],
343
},
344
},
345
cookies: [],
346
},
347
});
348
const tab = Session.getTab(meta);
349
Helpers.needsClosing.push(tab.session);
350
351
koaServer.get('/local-change-pre', ctx => {
352
ctx.body = `<body>
353
<h1>storage page</h1>
354
<a href="/local-change-post">Click</a>
355
<script>
356
localStorage.setItem('test', 'changed');
357
</script>
358
</body>`;
359
});
360
361
koaServer.get('/local-change-post', ctx => {
362
ctx.body = `<body>
363
<div id="local"></div>
364
<script>
365
document.querySelector('#local').innerHTML = localStorage.getItem('test');
366
</script>
367
</body>`;
368
});
369
370
await tab.goto(`${koaServer.baseUrl}/local-change-pre`);
371
await tab.waitForLoad('PaintingStable');
372
373
const profile = await connection.exportUserProfile(meta);
374
expect(profile.storage[koaServer.baseUrl]?.localStorage).toHaveLength(1);
375
expect(profile.storage[koaServer.baseUrl]?.localStorage[0][1]).toBe('changed');
376
377
await tab.interact([
378
{
379
command: InteractionCommand.click,
380
mousePosition: ['window', 'document', ['querySelector', 'a']],
381
},
382
]);
383
384
const localContent = await tab.execJsPath([
385
'document',
386
['querySelector', '#local'],
387
'textContent',
388
]);
389
expect(localContent.value).toBe('changed');
390
391
await tab.close();
392
});
393
394
it('should store cross domain domStorage items', async () => {
395
let profile: IUserProfile;
396
{
397
const meta = await connection.createSession();
398
const tab = Session.getTab(meta);
399
const session = tab.session;
400
Helpers.needsClosing.push(session);
401
session.mitmRequestSession.blockedResources = {
402
urls: ['http://dataliberationfoundation.org/*'],
403
types: [],
404
handlerFn: (request, response) => {
405
response.end(`<html><body><p>frame body</p>
406
<script>
407
localStorage.setItem('cross', '1');
408
</script>
409
</body>
410
</html>`);
411
return true;
412
},
413
};
414
415
koaServer.get('/cross-storage', ctx => {
416
ctx.body = `<body>
417
<div>Cross Storage</div>
418
<iframe src="http://dataliberationfoundation.org/storage"></iframe>
419
<script>
420
localStorage.setItem('local', '2');
421
</script>
422
</body>`;
423
});
424
425
await tab.goto(`${koaServer.baseUrl}/cross-storage`);
426
await tab.waitForLoad('PaintingStable');
427
await tab.puppetPage.frames[1].waitForLifecycleEvent('load');
428
profile = await connection.exportUserProfile(meta);
429
expect(profile.storage[koaServer.baseUrl]?.localStorage).toHaveLength(1);
430
expect(profile.storage['http://dataliberationfoundation.org']?.localStorage).toHaveLength(1);
431
await session.close();
432
}
433
{
434
const meta = await connection.createSession({
435
userProfile: profile,
436
});
437
const tab = Session.getTab(meta);
438
const session = tab.session;
439
Helpers.needsClosing.push(session);
440
441
session.mitmRequestSession.blockedResources = {
442
urls: ['http://dataliberationfoundation.org/*'],
443
types: [],
444
handlerFn: (request, response) => {
445
response.end(`<html>
446
<body>
447
<script>
448
window.parent.postMessage({message: localStorage.getItem('cross')}, "${koaServer.baseUrl}");
449
</script>
450
</body>
451
</html>`);
452
return true;
453
},
454
};
455
koaServer.get('/cross-storage2', ctx => {
456
ctx.body = `<body>
457
<div id="local"></div>
458
<div id="cross"></div>
459
<iframe src="http://dataliberationfoundation.org/storage2"></iframe>
460
<script>
461
window.addEventListener('message', function(event) {
462
document.querySelector('#cross').innerHTML = event.data.message;
463
document.querySelector('#cross').classList.add('ready');
464
});
465
document.querySelector('#local').innerHTML = localStorage.getItem('local');
466
</script>
467
</body>`;
468
});
469
await tab.goto(`${koaServer.baseUrl}/cross-storage2`);
470
await tab.waitForLoad('PaintingStable');
471
const localContent = await tab.execJsPath([
472
'document',
473
['querySelector', '#local'],
474
'textContent',
475
]);
476
expect(localContent.value).toBe('2');
477
478
await tab.waitForElement(['document', ['querySelector', '#cross.ready']]);
479
const crossContent = await tab.execJsPath([
480
'document',
481
['querySelector', '#cross'],
482
'textContent',
483
]);
484
expect(crossContent.value).toBe('1');
485
await session.close();
486
487
const history = tab.navigations.history;
488
expect(history).toHaveLength(1);
489
expect(history[0].finalUrl).toBe(`${koaServer.baseUrl}/cross-storage2`);
490
}
491
});
492
});
493
494
describe('UserProfile IndexedDb tests', () => {
495
it('should not fail on an empty db', async () => {
496
koaServer.get('/dbfail', ctx => {
497
ctx.body = `<body>
498
<h1>db page</h1>
499
<script>
500
const openDBRequest = indexedDB.open('dbfail', 1);
501
openDBRequest.onupgradeneeded = function() {
502
503
document.body.classList.add('ready');
504
505
506
}
507
</script>
508
</body>`;
509
});
510
511
{
512
const meta = await connection.createSession();
513
const tab = Session.getTab(meta);
514
Helpers.needsClosing.push(tab.session);
515
await tab.goto(`${koaServer.baseUrl}/dbfail`);
516
await tab.waitForLoad('PaintingStable');
517
await tab.waitForElement(['document', ['querySelector', 'body.ready']]);
518
519
await expect(connection.exportUserProfile(meta)).resolves.toBeTruthy();
520
}
521
});
522
523
it('should be able to save and restore an indexed db', async () => {
524
koaServer.get('/db', ctx => {
525
ctx.body = `<body>
526
<h1>db page</h1>
527
<script>
528
const openDBRequest = indexedDB.open('db1', 1);
529
openDBRequest.onupgradeneeded = function(ev) {
530
const db = ev.target.result;
531
const store1 = db.createObjectStore('store1', {
532
keyPath: 'id',
533
autoIncrement: false
534
});
535
store1.createIndex('store1_index1', ['child','name'], {
536
unique: false,
537
});
538
store1.createIndex('store1_index2', 'id', {
539
unique: true,
540
});
541
542
543
db.createObjectStore('store2');
544
function createStore2() {
545
const insertStore = db
546
.transaction('store2', 'readwrite')
547
.objectStore('store2');
548
insertStore.add(new Date(), '1');
549
insertStore.transaction.oncomplete = () => {
550
document.body.classList.add('ready');
551
}
552
}
553
554
store1.transaction.oncomplete = function() {
555
const insertStore = db
556
.transaction('store1', 'readwrite')
557
.objectStore('store1');
558
insertStore.add({ id: 1, child: { name: 'Richard', age: new Date() }});
559
insertStore.add({ id: 2, child: { name: 'Jill' } });
560
insertStore.transaction.oncomplete = () => {
561
createStore2();
562
}
563
};
564
565
}
566
</script>
567
</body>`;
568
});
569
570
koaServer.get('/dbrestore', ctx => {
571
ctx.body = `<body>
572
<div id="records"></div>
573
<div id="richard"></div>
574
<div id="date-type"></div>
575
<script>
576
function ready(){
577
document.body.classList.add('ready');
578
}
579
const openDBRequest = indexedDB.open('db1', 1);
580
openDBRequest.onsuccess = function(ev) {
581
const db = ev.target.result;
582
const tx = db.transaction('store1', 'readonly').objectStore('store1');
583
584
const recordQuery = tx.getAll();
585
586
const completed = new Set();
587
recordQuery.onsuccess = function({ target }) {
588
document.querySelector('#records').innerHTML = JSON.stringify(target.result);
589
completed.add('records');
590
if (completed.size === 2) {
591
ready();
592
}
593
};
594
595
const indexQuery = tx.index('store1_index2').get(1);
596
indexQuery.onsuccess = function({ target }) {
597
document.querySelector('#richard').innerHTML = JSON.stringify(target.result);
598
document.querySelector('#date-type').innerHTML = target.result.child.age.constructor.name;
599
completed.add('richard');
600
if (completed.size === 2) {
601
ready();
602
}
603
};
604
}
605
</script>
606
</body>`;
607
});
608
609
let profile: IUserProfile;
610
{
611
const meta = await connection.createSession();
612
const tab = Session.getTab(meta);
613
Helpers.needsClosing.push(tab.session);
614
await tab.goto(`${koaServer.baseUrl}/db`);
615
await tab.waitForLoad('PaintingStable');
616
await tab.waitForElement(['document', ['querySelector', 'body.ready']]);
617
618
profile = await connection.exportUserProfile(meta);
619
expect(profile.storage[koaServer.baseUrl]?.indexedDB).toHaveLength(1);
620
const db = profile.storage[koaServer.baseUrl]?.indexedDB[0];
621
expect(db.name).toBe('db1');
622
expect(db.version).toBe(1);
623
expect(db.objectStores).toHaveLength(2);
624
expect(db.objectStores[0].name).toBe('store1');
625
expect(db.objectStores[0].keyPath).toBe('id');
626
expect(db.objectStores[0].indexes).toHaveLength(2);
627
expect(db.objectStores[0].indexes[0].keyPath).toStrictEqual(['child', 'name']);
628
expect(db.objectStores[1].name).toBe('store2');
629
expect(db.objectStores[1].keyPath).not.toBeTruthy();
630
631
expect(db.data.store1).toHaveLength(2);
632
}
633
{
634
const meta = await connection.createSession({
635
userProfile: profile,
636
});
637
const tab = Session.getTab(meta);
638
Helpers.needsClosing.push(tab.session);
639
640
await tab.goto(`${koaServer.baseUrl}/dbrestore`);
641
await tab.waitForLoad('PaintingStable');
642
await tab.waitForElement(['document', ['querySelector', 'body.ready']]);
643
644
const recordsJson = await tab.execJsPath<string>([
645
'document',
646
['querySelector', '#records'],
647
'textContent',
648
]);
649
const records = JSON.parse(recordsJson.value);
650
651
expect(records).toHaveLength(2);
652
expect(records[0].child.name).toBe('Richard');
653
const indexLookupJson = await tab.execJsPath<string>([
654
'document',
655
['querySelector', '#richard'],
656
'textContent',
657
]);
658
const indexLookup = JSON.parse(indexLookupJson.value);
659
expect(indexLookup.id).toBe(1);
660
661
const typePreservation = await tab.execJsPath([
662
'document',
663
['querySelector', '#date-type'],
664
'textContent',
665
]);
666
expect(typePreservation.value).toBe('Date');
667
}
668
});
669
});
670
671