Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fastify.inject like testing for WS? #35

Closed
gjovanov opened this issue Aug 27, 2019 · 13 comments · Fixed by #276
Closed

fastify.inject like testing for WS? #35

gjovanov opened this issue Aug 27, 2019 · 13 comments · Fixed by #276
Labels
good first issue Good for newcomers help wanted Help the community by contributing to this issue

Comments

@gjovanov
Copy link

Hey guys,
for faking HTTP requests, there is ab option to inject fake HTTP requests via light-my-request. Looking in the tests of this repo, I can see that Fastify really needs to listen on some port in order to create a WS client connetion.

Is there a way to Inject a WS connection without actually making Fastify server listen on a port?

I'm asking because right now we many HTTP tests using Ava testing framework, that are in many separate files (due to concurrent testing). So far for HTTP testing we were not forcing the Fastify server to listen on any port. Now we are at the point where we want to start cover the WS scenarios with such tests and it seems that we are going to need to actually make Fastify server listen on certain port.

The issue we are seeing is that each test file will need to have a separate port, because they run concurrently. I know it's doable like that, but I'm just currious if it's possible to fake WS connections as well as WS data being sent?

Thanks in advance.

/GJ

@mcollina
Copy link
Member

That would be a fantastic feature! Would you like to send a PR? The way I would build this is to use https://github.com/mafintosh/duplexify to create a couple of "inverted" duplex streams from a couple of PassThrough. Then, we can issue an 'upgrade' event on the server.
Wdyt? Would you like to send a PR?

@gjovanov
Copy link
Author

gjovanov commented Aug 28, 2019

Hey @mcollina, indeed it would be a useful feature.

So far I haven't had a look in the source code of Fastify, so I decided to have a look in both Fastify and Duplexify, to evaluate a potential contribution via a PR. At first glance this seems too overwhelming, considering other tasks on my plate.

Maybe if you help me out providing some hints where to start looking in the source code options for checking PassThrough requests and issuing events on the server? Also what would be the flow of establishing a WS? E.g. PassThrough request with A, B, C headers etc, then Upgrade request with X, Y, Z headers?

@mcollina
Copy link
Member

Essentially you'll need to add a decorator that calls https://github.com/fastify/fastify-websocket/blob/master/index.js#L63-L67 with the right arguments.

@stale
Copy link

stale bot commented Oct 21, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Oct 21, 2020
@stale stale bot closed this as completed Nov 7, 2020
@mcollina mcollina added good first issue Good for newcomers help wanted Help the community by contributing to this issue labels Nov 7, 2020
@mcollina mcollina reopened this Nov 7, 2020
@stale stale bot removed the stale label Nov 7, 2020
@airhorns
Copy link
Member

There's also the mock-socket library here (https://github.com/thoov/mock-socket), which I've had good success with. If we made the server implementation an option, you could maybe just inject the mock server and use the mock socket client which quacks more like a normal WebSocket and handles all the internal event emission for you.

@mcollina
Copy link
Member

That'd work! Would you like to send a PR?

@wyozi
Copy link
Contributor

wyozi commented Apr 23, 2021

Ideally, I'd be very happy with something like

const ws = fastify.injectWebSocket('/my/ws/path')
ws.send('hello world') // just a normal WS instance

without having to touch the original plugin options at all.

I don't think this would be doable with the ws library though, except maybe hacking with passing a null address to the WS client somehow to make it operate in server mode?

@mcollina
Copy link
Member

I have not designed this :(. It probably requires some analysis and experimentation.

@wyozi
Copy link
Contributor

wyozi commented Apr 24, 2021

I looked into this a bit but stopped once it started feeling like stacking hacks on top of hacks. I got to the point where it kind of manages to setup a WS connection, but sending data doesn't quite work yet. Maybe this will be a useful starting point for something else

  fastify.decorate('injectWebSocket', opts => {
    const client = new WebSocket(null)

    const server2client = new PassThrough()
    const client2server = new PassThrough()
    const serverStream = new class extends duplexify {
      constructor() {
        super(server2client, client2server)
      }
      setTimeout() {}
      setNoDelay() {}
    }
    const clientStream = new class extends duplexify {
      constructor() {
        super(client2server, server2client)
      }
      setTimeout() {}
      setNoDelay() {}
    }

    let serverClient
    wss.handleUpgrade({
      method: 'GET',
      headers: {
        connection: 'upgrade',
        upgrade: 'websocket',
        'sec-websocket-version': 13,
        'sec-websocket-key': randomBytes(16).toString('base64')
      },
    }, serverStream, Buffer.from([]), (ws, req) => { serverClient = ws })


    let resolve
    const promise = new Promise(_resolve => resolve = _resolve)

    function recvData(d) {
      if (d.toString().includes('101 Switching')) {
        clientStream.removeListener('data', recvData)
        client.setSocket(clientStream, Buffer.from([]))
        client._isServer = false // yikes
        resolve(client)
      }
    }
    clientStream.on('data', recvData)

    // TODO how to get the route handler
    testesttest(WebSocket.createWebSocketStream(serverStream))

    return promise
  })

@gustawdaniel
Copy link

gustawdaniel commented Mar 7, 2023

Implemented message handling it in my project so I will share results. I hope it will be helpful for implementation:

Test in jest

import WebSocket from 'ws'
import fastifyWebsocket, { SocketStream } from '@fastify/websocket'
import fastify from 'fastify'
import { Reactivity } from '../src/services/Reactivity'

describe('websocket', () => {  
  it('basic', async () => {
    let expectedMessage = ''

    const app = fastify()

    app.register(fastifyWebsocket)

    const rawContext: { connection: SocketStream | null } = {
      connection: null,
    }

    const [ctx, establishConnection] = Reactivity.useFirstChangeDetector(rawContext)

    app.get('/ws', { websocket: true }, (connection) => {
      connection.socket.on('message', async (message) => {
        expectedMessage = message.toString()
      })
      ctx.connection = connection
    })

    await app.ready()
    const address = await app.listen({ port: 0, host: '0.0.0.0' })
    new WebSocket(`${address.replace('http', 'ws')}/ws`)

    await establishConnection;

    ctx.connection?.socket.emit('message', 'xd')

    await app.close()
    expect(expectedMessage).toEqual('xd')
  })
})

and Reactivity helper

import { EventEmitter } from 'node:events'

export class Reactivity {
  static useFirstChangeDetector<T extends object>(target: T): [T, Promise<void>] {
    const e = new EventEmitter()

    const handler: ProxyHandler<T> = {
      set(obj, prop, value) {
        e.emit('change', prop, value)
        return Reflect.set(obj, prop, value)
      },
    }

    const proxy = new Proxy(target, handler)

    return [
      proxy,
      new Promise((resolve) => {
        e.on('change', () => {
          resolve(undefined)
        })
      }),
    ]
  }
}

with his own test

import dayjs from 'dayjs'
import { sleep } from './helpers'
import { Reactivity } from '../src/services/Reactivity'

describe('reactivity', () => {
  it('i am able to react on change in object instantly instead of using setTimeout', async () => {
    const target = {
      value: 'Why do programmers prefer dark mode?',
    }

    const [proxy, promise] = Reactivity.useFirstChangeDetector(target)

    let t2: dayjs.Dayjs = dayjs()

    promise.then(() => {
      t2 = dayjs()
    })

    const t1 = dayjs()
    await sleep(1)
    proxy.value = 'Because light attracts bugs.'
    await sleep(1)
    const t3 = dayjs()

    expect(t1.valueOf()).toBeLessThan(t2.valueOf())
    expect(t2.valueOf()).toBeLessThan(t3.valueOf())
    expect(proxy.value).toEqual('Because light attracts bugs.')
  })
})

with sleep function as

export const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))

What I mostyly dislike in my code

    await app.listen({ port: 9998, host: '0.0.0.0' })
    new WebSocket('ws://0.0.0.0:9998/ws')

I can't do it without reservation of port. If you can please share it with me. Without these lines on connection is not triggered, but when I aim triggering it by emit then cant prepare object that will be correct socket connection as first argument of handler.

Btw this proof of concept shows that we are able to test messages to websocket.

@Eomm Eomm removed the hacktoberfest label Apr 1, 2023
@Sbax
Copy link

Sbax commented May 9, 2023

Looked a bit into it trying to come up with a solution but hit a wall trying to have fastify-websocket working without reserving a port or starting the server.
It looks like the whole fastify-websocket process only really starts once the server is running so the connection event won't trigger until then.

Also tried replacing the inner server with a mocked one provided externally through options but it isn't enough to solve this issue since fastify ws routes aren't really attached until the server is running.

@mcollina
Copy link
Member

I think what this feature should do is to emit an upgrade event in

websocketListenServer.on('upgrade', onUpgrade)
.

If the API match up, every should line up correctly.

@matt-clarson
Copy link

i tried emitting the upgrade event, but encountered a similar issue that others are describing here that it seems very tricky to separate the websocket implementation from needing a running http.Server instance

@DanieleFedeli DanieleFedeli mentioned this issue Nov 6, 2023
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
good first issue Good for newcomers help wanted Help the community by contributing to this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants