1/**
2 * Copyright 2018 Google Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import fs from 'fs';
18import path from 'path';
19import utils from './utils.js';
20import expect from 'expect';
21import {
22  getTestState,
23  setupTestBrowserHooks,
24  setupTestPageAndContextHooks,
25  itFailsFirefox,
26  describeFailsFirefox,
27} from './mocha-utils'; // eslint-disable-line import/extensions
28
29describe('network', function () {
30  setupTestBrowserHooks();
31  setupTestPageAndContextHooks();
32
33  describe('Page.Events.Request', function () {
34    it('should fire for navigation requests', async () => {
35      const { page, server } = getTestState();
36
37      const requests = [];
38      page.on(
39        'request',
40        (request) => !utils.isFavicon(request) && requests.push(request)
41      );
42      await page.goto(server.EMPTY_PAGE);
43      expect(requests.length).toBe(1);
44    });
45    it('should fire for iframes', async () => {
46      const { page, server } = getTestState();
47
48      const requests = [];
49      page.on(
50        'request',
51        (request) => !utils.isFavicon(request) && requests.push(request)
52      );
53      await page.goto(server.EMPTY_PAGE);
54      await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
55      expect(requests.length).toBe(2);
56    });
57    it('should fire for fetches', async () => {
58      const { page, server } = getTestState();
59
60      const requests = [];
61      page.on(
62        'request',
63        (request) => !utils.isFavicon(request) && requests.push(request)
64      );
65      await page.goto(server.EMPTY_PAGE);
66      await page.evaluate(() => fetch('/empty.html'));
67      expect(requests.length).toBe(2);
68    });
69  });
70
71  describe('Request.frame', function () {
72    it('should work for main frame navigation request', async () => {
73      const { page, server } = getTestState();
74
75      const requests = [];
76      page.on(
77        'request',
78        (request) => !utils.isFavicon(request) && requests.push(request)
79      );
80      await page.goto(server.EMPTY_PAGE);
81      expect(requests.length).toBe(1);
82      expect(requests[0].frame()).toBe(page.mainFrame());
83    });
84    it('should work for subframe navigation request', async () => {
85      const { page, server } = getTestState();
86
87      await page.goto(server.EMPTY_PAGE);
88      const requests = [];
89      page.on(
90        'request',
91        (request) => !utils.isFavicon(request) && requests.push(request)
92      );
93      await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
94      expect(requests.length).toBe(1);
95      expect(requests[0].frame()).toBe(page.frames()[1]);
96    });
97    it('should work for fetch requests', async () => {
98      const { page, server } = getTestState();
99
100      await page.goto(server.EMPTY_PAGE);
101      let requests = [];
102      page.on(
103        'request',
104        (request) => !utils.isFavicon(request) && requests.push(request)
105      );
106      await page.evaluate(() => fetch('/digits/1.png'));
107      requests = requests.filter(
108        (request) => !request.url().includes('favicon')
109      );
110      expect(requests.length).toBe(1);
111      expect(requests[0].frame()).toBe(page.mainFrame());
112    });
113  });
114
115  describe('Request.headers', function () {
116    it('should work', async () => {
117      const { page, server, isChrome } = getTestState();
118
119      const response = await page.goto(server.EMPTY_PAGE);
120      if (isChrome)
121        expect(response.request().headers()['user-agent']).toContain('Chrome');
122      else
123        expect(response.request().headers()['user-agent']).toContain('Firefox');
124    });
125  });
126
127  describe('Response.headers', function () {
128    it('should work', async () => {
129      const { page, server } = getTestState();
130
131      server.setRoute('/empty.html', (req, res) => {
132        res.setHeader('foo', 'bar');
133        res.end();
134      });
135      const response = await page.goto(server.EMPTY_PAGE);
136      expect(response.headers()['foo']).toBe('bar');
137    });
138  });
139
140  describe('Response.fromCache', function () {
141    it('should return |false| for non-cached content', async () => {
142      const { page, server } = getTestState();
143
144      const response = await page.goto(server.EMPTY_PAGE);
145      expect(response.fromCache()).toBe(false);
146    });
147
148    it('should work', async () => {
149      const { page, server } = getTestState();
150
151      const responses = new Map();
152      page.on(
153        'response',
154        (r) =>
155          !utils.isFavicon(r.request()) &&
156          responses.set(r.url().split('/').pop(), r)
157      );
158
159      // Load and re-load to make sure it's cached.
160      await page.goto(server.PREFIX + '/cached/one-style.html');
161      await page.reload();
162
163      expect(responses.size).toBe(2);
164      expect(responses.get('one-style.css').status()).toBe(200);
165      expect(responses.get('one-style.css').fromCache()).toBe(true);
166      expect(responses.get('one-style.html').status()).toBe(304);
167      expect(responses.get('one-style.html').fromCache()).toBe(false);
168    });
169  });
170
171  describe('Response.fromServiceWorker', function () {
172    it('should return |false| for non-service-worker content', async () => {
173      const { page, server } = getTestState();
174
175      const response = await page.goto(server.EMPTY_PAGE);
176      expect(response.fromServiceWorker()).toBe(false);
177    });
178
179    it('Response.fromServiceWorker', async () => {
180      const { page, server } = getTestState();
181
182      const responses = new Map();
183      page.on('response', (r) => responses.set(r.url().split('/').pop(), r));
184
185      // Load and re-load to make sure serviceworker is installed and running.
186      await page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html', {
187        waitUntil: 'networkidle2',
188      });
189      await page.evaluate(async () => await globalThis.activationPromise);
190      await page.reload();
191
192      expect(responses.size).toBe(2);
193      expect(responses.get('sw.html').status()).toBe(200);
194      expect(responses.get('sw.html').fromServiceWorker()).toBe(true);
195      expect(responses.get('style.css').status()).toBe(200);
196      expect(responses.get('style.css').fromServiceWorker()).toBe(true);
197    });
198  });
199
200  describe('Request.postData', function () {
201    it('should work', async () => {
202      const { page, server } = getTestState();
203
204      await page.goto(server.EMPTY_PAGE);
205      server.setRoute('/post', (req, res) => res.end());
206      let request = null;
207      page.on('request', (r) => (request = r));
208      await page.evaluate(() =>
209        fetch('./post', {
210          method: 'POST',
211          body: JSON.stringify({ foo: 'bar' }),
212        })
213      );
214      expect(request).toBeTruthy();
215      expect(request.postData()).toBe('{"foo":"bar"}');
216    });
217    it('should be |undefined| when there is no post data', async () => {
218      const { page, server } = getTestState();
219
220      const response = await page.goto(server.EMPTY_PAGE);
221      expect(response.request().postData()).toBe(undefined);
222    });
223  });
224
225  describe('Response.text', function () {
226    it('should work', async () => {
227      const { page, server } = getTestState();
228
229      const response = await page.goto(server.PREFIX + '/simple.json');
230      const responseText = (await response.text()).trimEnd();
231      expect(responseText).toBe('{"foo": "bar"}');
232    });
233    it('should return uncompressed text', async () => {
234      const { page, server } = getTestState();
235
236      server.enableGzip('/simple.json');
237      const response = await page.goto(server.PREFIX + '/simple.json');
238      expect(response.headers()['content-encoding']).toBe('gzip');
239      const responseText = (await response.text()).trimEnd();
240      expect(responseText).toBe('{"foo": "bar"}');
241    });
242    it('should throw when requesting body of redirected response', async () => {
243      const { page, server } = getTestState();
244
245      server.setRedirect('/foo.html', '/empty.html');
246      const response = await page.goto(server.PREFIX + '/foo.html');
247      const redirectChain = response.request().redirectChain();
248      expect(redirectChain.length).toBe(1);
249      const redirected = redirectChain[0].response();
250      expect(redirected.status()).toBe(302);
251      let error = null;
252      await redirected.text().catch((error_) => (error = error_));
253      expect(error.message).toContain(
254        'Response body is unavailable for redirect responses'
255      );
256    });
257    it('should wait until response completes', async () => {
258      const { page, server } = getTestState();
259
260      await page.goto(server.EMPTY_PAGE);
261      // Setup server to trap request.
262      let serverResponse = null;
263      server.setRoute('/get', (req, res) => {
264        serverResponse = res;
265        // In Firefox, |fetch| will be hanging until it receives |Content-Type| header
266        // from server.
267        res.setHeader('Content-Type', 'text/plain; charset=utf-8');
268        res.write('hello ');
269      });
270      // Setup page to trap response.
271      let requestFinished = false;
272      page.on(
273        'requestfinished',
274        (r) => (requestFinished = requestFinished || r.url().includes('/get'))
275      );
276      // send request and wait for server response
277      const [pageResponse] = await Promise.all([
278        page.waitForResponse((r) => !utils.isFavicon(r.request())),
279        page.evaluate(() => fetch('./get', { method: 'GET' })),
280        server.waitForRequest('/get'),
281      ]);
282
283      expect(serverResponse).toBeTruthy();
284      expect(pageResponse).toBeTruthy();
285      expect(pageResponse.status()).toBe(200);
286      expect(requestFinished).toBe(false);
287
288      const responseText = pageResponse.text();
289      // Write part of the response and wait for it to be flushed.
290      await new Promise((x) => serverResponse.write('wor', x));
291      // Finish response.
292      await new Promise((x) => serverResponse.end('ld!', x));
293      expect(await responseText).toBe('hello world!');
294    });
295  });
296
297  describe('Response.json', function () {
298    it('should work', async () => {
299      const { page, server } = getTestState();
300
301      const response = await page.goto(server.PREFIX + '/simple.json');
302      expect(await response.json()).toEqual({ foo: 'bar' });
303    });
304  });
305
306  describe('Response.buffer', function () {
307    it('should work', async () => {
308      const { page, server } = getTestState();
309
310      const response = await page.goto(server.PREFIX + '/pptr.png');
311      const imageBuffer = fs.readFileSync(
312        path.join(__dirname, 'assets', 'pptr.png')
313      );
314      const responseBuffer = await response.buffer();
315      expect(responseBuffer.equals(imageBuffer)).toBe(true);
316    });
317    it('should work with compression', async () => {
318      const { page, server } = getTestState();
319
320      server.enableGzip('/pptr.png');
321      const response = await page.goto(server.PREFIX + '/pptr.png');
322      const imageBuffer = fs.readFileSync(
323        path.join(__dirname, 'assets', 'pptr.png')
324      );
325      const responseBuffer = await response.buffer();
326      expect(responseBuffer.equals(imageBuffer)).toBe(true);
327    });
328  });
329
330  describe('Response.statusText', function () {
331    it('should work', async () => {
332      const { page, server } = getTestState();
333
334      server.setRoute('/cool', (req, res) => {
335        res.writeHead(200, 'cool!');
336        res.end();
337      });
338      const response = await page.goto(server.PREFIX + '/cool');
339      expect(response.statusText()).toBe('cool!');
340    });
341  });
342
343  describe('Network Events', function () {
344    it('Page.Events.Request', async () => {
345      const { page, server } = getTestState();
346
347      const requests = [];
348      page.on('request', (request) => requests.push(request));
349      await page.goto(server.EMPTY_PAGE);
350      expect(requests.length).toBe(1);
351      expect(requests[0].url()).toBe(server.EMPTY_PAGE);
352      expect(requests[0].resourceType()).toBe('document');
353      expect(requests[0].method()).toBe('GET');
354      expect(requests[0].response()).toBeTruthy();
355      expect(requests[0].frame() === page.mainFrame()).toBe(true);
356      expect(requests[0].frame().url()).toBe(server.EMPTY_PAGE);
357    });
358    it('Page.Events.RequestServedFromCache', async () => {
359      const { page, server } = getTestState();
360
361      const cached = [];
362      page.on('requestservedfromcache', (r) =>
363        cached.push(r.url().split('/').pop())
364      );
365
366      await page.goto(server.PREFIX + '/cached/one-style.html');
367      expect(cached).toEqual([]);
368
369      await page.reload();
370      expect(cached).toEqual(['one-style.css']);
371    });
372    it('Page.Events.Response', async () => {
373      const { page, server } = getTestState();
374
375      const responses = [];
376      page.on('response', (response) => responses.push(response));
377      await page.goto(server.EMPTY_PAGE);
378      expect(responses.length).toBe(1);
379      expect(responses[0].url()).toBe(server.EMPTY_PAGE);
380      expect(responses[0].status()).toBe(200);
381      expect(responses[0].ok()).toBe(true);
382      expect(responses[0].request()).toBeTruthy();
383      const remoteAddress = responses[0].remoteAddress();
384      // Either IPv6 or IPv4, depending on environment.
385      expect(
386        remoteAddress.ip.includes('::1') || remoteAddress.ip === '127.0.0.1'
387      ).toBe(true);
388      expect(remoteAddress.port).toBe(server.PORT);
389    });
390
391    it('Page.Events.RequestFailed', async () => {
392      const { page, server, isChrome } = getTestState();
393
394      await page.setRequestInterception(true);
395      page.on('request', (request) => {
396        if (request.url().endsWith('css')) request.abort();
397        else request.continue();
398      });
399      const failedRequests = [];
400      page.on('requestfailed', (request) => failedRequests.push(request));
401      await page.goto(server.PREFIX + '/one-style.html');
402      expect(failedRequests.length).toBe(1);
403      expect(failedRequests[0].url()).toContain('one-style.css');
404      expect(failedRequests[0].response()).toBe(null);
405      expect(failedRequests[0].resourceType()).toBe('stylesheet');
406      if (isChrome)
407        expect(failedRequests[0].failure().errorText).toBe('net::ERR_FAILED');
408      else
409        expect(failedRequests[0].failure().errorText).toBe('NS_ERROR_FAILURE');
410      expect(failedRequests[0].frame()).toBeTruthy();
411    });
412    it('Page.Events.RequestFinished', async () => {
413      const { page, server } = getTestState();
414
415      const requests = [];
416      page.on('requestfinished', (request) => requests.push(request));
417      await page.goto(server.EMPTY_PAGE);
418      expect(requests.length).toBe(1);
419      expect(requests[0].url()).toBe(server.EMPTY_PAGE);
420      expect(requests[0].response()).toBeTruthy();
421      expect(requests[0].frame() === page.mainFrame()).toBe(true);
422      expect(requests[0].frame().url()).toBe(server.EMPTY_PAGE);
423    });
424    it('should fire events in proper order', async () => {
425      const { page, server } = getTestState();
426
427      const events = [];
428      page.on('request', () => events.push('request'));
429      page.on('response', () => events.push('response'));
430      page.on('requestfinished', () => events.push('requestfinished'));
431      await page.goto(server.EMPTY_PAGE);
432      expect(events).toEqual(['request', 'response', 'requestfinished']);
433    });
434    it('should support redirects', async () => {
435      const { page, server } = getTestState();
436
437      const events = [];
438      page.on('request', (request) =>
439        events.push(`${request.method()} ${request.url()}`)
440      );
441      page.on('response', (response) =>
442        events.push(`${response.status()} ${response.url()}`)
443      );
444      page.on('requestfinished', (request) =>
445        events.push(`DONE ${request.url()}`)
446      );
447      page.on('requestfailed', (request) =>
448        events.push(`FAIL ${request.url()}`)
449      );
450      server.setRedirect('/foo.html', '/empty.html');
451      const FOO_URL = server.PREFIX + '/foo.html';
452      const response = await page.goto(FOO_URL);
453      expect(events).toEqual([
454        `GET ${FOO_URL}`,
455        `302 ${FOO_URL}`,
456        `DONE ${FOO_URL}`,
457        `GET ${server.EMPTY_PAGE}`,
458        `200 ${server.EMPTY_PAGE}`,
459        `DONE ${server.EMPTY_PAGE}`,
460      ]);
461
462      // Check redirect chain
463      const redirectChain = response.request().redirectChain();
464      expect(redirectChain.length).toBe(1);
465      expect(redirectChain[0].url()).toContain('/foo.html');
466      expect(redirectChain[0].response().remoteAddress().port).toBe(
467        server.PORT
468      );
469    });
470  });
471
472  describe('Request.isNavigationRequest', () => {
473    it('should work', async () => {
474      const { page, server } = getTestState();
475
476      const requests = new Map();
477      page.on('request', (request) =>
478        requests.set(request.url().split('/').pop(), request)
479      );
480      server.setRedirect('/rrredirect', '/frames/one-frame.html');
481      await page.goto(server.PREFIX + '/rrredirect');
482      expect(requests.get('rrredirect').isNavigationRequest()).toBe(true);
483      expect(requests.get('one-frame.html').isNavigationRequest()).toBe(true);
484      expect(requests.get('frame.html').isNavigationRequest()).toBe(true);
485      expect(requests.get('script.js').isNavigationRequest()).toBe(false);
486      expect(requests.get('style.css').isNavigationRequest()).toBe(false);
487    });
488    it('should work with request interception', async () => {
489      const { page, server } = getTestState();
490
491      const requests = new Map();
492      page.on('request', (request) => {
493        requests.set(request.url().split('/').pop(), request);
494        request.continue();
495      });
496      await page.setRequestInterception(true);
497      server.setRedirect('/rrredirect', '/frames/one-frame.html');
498      await page.goto(server.PREFIX + '/rrredirect');
499      expect(requests.get('rrredirect').isNavigationRequest()).toBe(true);
500      expect(requests.get('one-frame.html').isNavigationRequest()).toBe(true);
501      expect(requests.get('frame.html').isNavigationRequest()).toBe(true);
502      expect(requests.get('script.js').isNavigationRequest()).toBe(false);
503      expect(requests.get('style.css').isNavigationRequest()).toBe(false);
504    });
505    it('should work when navigating to image', async () => {
506      const { page, server } = getTestState();
507
508      const requests = [];
509      page.on('request', (request) => requests.push(request));
510      await page.goto(server.PREFIX + '/pptr.png');
511      expect(requests[0].isNavigationRequest()).toBe(true);
512    });
513  });
514
515  describe('Page.setExtraHTTPHeaders', function () {
516    it('should work', async () => {
517      const { page, server } = getTestState();
518
519      await page.setExtraHTTPHeaders({
520        foo: 'bar',
521      });
522      const [request] = await Promise.all([
523        server.waitForRequest('/empty.html'),
524        page.goto(server.EMPTY_PAGE),
525      ]);
526      expect(request.headers['foo']).toBe('bar');
527    });
528    it('should throw for non-string header values', async () => {
529      const { page } = getTestState();
530
531      let error = null;
532      try {
533        // @ts-expect-error purposeful bad input
534        await page.setExtraHTTPHeaders({ foo: 1 });
535      } catch (error_) {
536        error = error_;
537      }
538      expect(error.message).toBe(
539        'Expected value of header "foo" to be String, but "number" is found.'
540      );
541    });
542  });
543
544  describe('Page.authenticate', function () {
545    it('should work', async () => {
546      const { page, server } = getTestState();
547
548      server.setAuth('/empty.html', 'user', 'pass');
549      let response = await page.goto(server.EMPTY_PAGE);
550      expect(response.status()).toBe(401);
551      await page.authenticate({
552        username: 'user',
553        password: 'pass',
554      });
555      response = await page.reload();
556      expect(response.status()).toBe(200);
557    });
558    it('should fail if wrong credentials', async () => {
559      const { page, server } = getTestState();
560
561      // Use unique user/password since Chrome caches credentials per origin.
562      server.setAuth('/empty.html', 'user2', 'pass2');
563      await page.authenticate({
564        username: 'foo',
565        password: 'bar',
566      });
567      const response = await page.goto(server.EMPTY_PAGE);
568      expect(response.status()).toBe(401);
569    });
570    it('should allow disable authentication', async () => {
571      const { page, server } = getTestState();
572
573      // Use unique user/password since Chrome caches credentials per origin.
574      server.setAuth('/empty.html', 'user3', 'pass3');
575      await page.authenticate({
576        username: 'user3',
577        password: 'pass3',
578      });
579      let response = await page.goto(server.EMPTY_PAGE);
580      expect(response.status()).toBe(200);
581      await page.authenticate(null);
582      // Navigate to a different origin to bust Chrome's credential caching.
583      response = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
584      expect(response.status()).toBe(401);
585    });
586    it('should not disable caching', async () => {
587      const { page, server } = getTestState();
588
589      // Use unique user/password since Chrome caches credentials per origin.
590      server.setAuth('/cached/one-style.css', 'user4', 'pass4');
591      server.setAuth('/cached/one-style.html', 'user4', 'pass4');
592      await page.authenticate({
593        username: 'user4',
594        password: 'pass4',
595      });
596
597      const responses = new Map();
598      page.on('response', (r) => responses.set(r.url().split('/').pop(), r));
599
600      // Load and re-load to make sure it's cached.
601      await page.goto(server.PREFIX + '/cached/one-style.html');
602      await page.reload();
603
604      expect(responses.get('one-style.css').status()).toBe(200);
605      expect(responses.get('one-style.css').fromCache()).toBe(true);
606      expect(responses.get('one-style.html').status()).toBe(304);
607      expect(responses.get('one-style.html').fromCache()).toBe(false);
608    });
609  });
610});
611