browserstack / browserstack-local-nodejs

NodeJS bindings for BrowserStack Local
https://www.browserstack.com
MIT License
71 stars 56 forks source link

Bug: cannot connect to secure web socket server spun up during test run #143

Open GrayedFox opened 1 year ago

GrayedFox commented 1 year ago

Details

There is no documented wsLocalSuport capability on the PlayWright page: https://www.browserstack.com/docs/automate/playwright/playwright-capabilities

I am having a similar issue to #106. Our test suite uses the ws package to spin up a web socket server which our AUT connects to when under test.

Everything works fine locally, and I have followed the advice on that thread, but still get the following error inside BrowserStack logs:

ERROR: WebSocket connection to 'wss://bs-local.com:12516/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED
ERROR: CloseEvent

Here is the web socket and server implementation:

import { Page } from '@playwright/test';
import fs from 'fs';
import https from 'https';
import { Mockmock } from 'mockmock';
import WebSocket, { WebSocketServer } from 'ws';

import { ClientData } from '../test-data';
import { isBrowserStack } from './browserstack';

const MM = Mockmock.instance;

/**
 * Sets up a dummy WebSocket server that responds with web socket data
 */
const setUpWebSocketServer = (port: number, socketId: string) => {
  const server = https.createServer({
    cert: fs.readFileSync('playwright/support/certs/cert.pem'),
    key: fs.readFileSync('playwright/support/certs/key.pem'),
  });

  server.listen(port, () => {
    console.log(`HTTPS server listening on port ${port}`);
  });

  const wss = new WebSocketServer({ server, verifyClient: () => true });

  // replays mocked data every 250ms
  wss.on('connection', (ws: WebSocket) => {
    // recursively replays mocked data until we run out of msgs
    const recursivelySendData = () => {
      const frame = MM.replay(socketId, 'data');
      if (typeof frame === 'undefined') {
        return;
      }
      ws.send(JSON.stringify(frame.mock));
      setTimeout(recursivelySendData, 250);
    };
    recursivelySendData();

    ws.on('error', (err) => {
      console.error(`Connetion error: ${err.message} with name ${err.name}:
      ${err.stack}`);
    });
  });
};

/**
 * Here we link PlayWright logic and MockMock by intercepting all WebSocket requests.
 * This is called by the setUp() method.
 *
 * @param page the page instance
 * @param port defaults to 3030
 * @param url defaults to appropriate environment websocket Url
 */
export const interceptWebSocketRequests = async (
  page: Page,
  port = 3030,
  url = ClientData.websocketUrl
) => {
  const socketId = 'wsData';
  const host = isBrowserStack() ? 'bs-local.com' : 'localhost';
  const wssUrl = MM.isReplaying ? `wss://${host}:${port}` : url;

  // if Mockmock is recording sniff all websocket frames and save them
  if (MM.isRecording) {
    page.on('websocket', (ws) => {
      ws.on('framereceived', (data) => {
        MM.record(socketId, JSON.parse(data.payload.toString()));
      });
    });
  }

  // if Mockmock replaying we set up a local webwocket server and route traffic to it
  if (MM.isReplaying) {
    // intercept the payments form, replace the wss url, and relax CS policies
    await page.route(`${ClientData.paymentsUrl}/**`, async (route) => {
      console.log(
        `Intercepted payments URL request to ${route.request().url()}`
      );
      const response = await route.fetch();
      const frameHtml = await response.text();
      const body = frameHtml.replace(ClientData.websocketUrl, wssUrl);
      const headers = response.headers();
      const csp = headers['content-security-policy'];

      // make csp more permissive
      headers['content-security-policy'] = csp.replace(
        "connect-src 'self'",
        "connect-src 'self' localhost:* wss://localhost:* bs-local.com:* wss://bs-local.com:*"
      );

      console.log(`Fulfilling with
      wss url: ${wssUrl}
      edited body has wss url: ${body.includes(wssUrl)}
      headers: ${JSON.stringify(headers)}
      `);

      await route.fulfill({ response, body, headers });
    });

    setUpWebSocketServer(port, socketId);
  }
};

Expected Behavior

The AUT, when it submits the payment form, should connect to the local secure web socket server and work the same way it does when running the tests locally.

Actual Behavior

The web socket server errors when a connection attempt is made.

ERROR: WebSocket connection to 'wss://bs-local.com:46300/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED
ERROR: CloseEvent

Steps to Reproduce the Problem

  1. Create a test file that acts as an AUT that connects to the secure web socket server - I can't share our application code as it's IP, but that part shouldn't be too hard.

  2. Write a test that triggers the AUT connecting to your ws secure web socket server.

Platform details

  1. playwright version: 1.33.0
  2. node version: 18.9.1
  3. os type and version: Ubuuntu 22.04.2 LTS
sharutkarsh commented 1 year ago

Hi @GrayedFox Kindly please raise a support ticket by sending an email to support@browserstack.com or via Contact Us here.

olekach commented 1 month ago

@GrayedFox Hi! Is await page.route(frameUrl, async (route) => { still working for you? For me, it is ignoring any wss urls.