typelevel / cats-effect

The pure asynchronous runtime for Scala
https://typelevel.org/cats-effect/
Apache License 2.0
2.03k stars 520 forks source link

TestKit: how to set the Clock? #3309

Open bblfish opened 1 year ago

bblfish commented 1 year ago

Use case: I have am writing an implementation of IETF http message signatures. The spec has a lot of examples of signed HTTP messages signed with a validity period. To test the library I would like to set the Clock to a specific valid time and to invalid times, to check that the implementation is correct, i.e. that it fails a correct signature when the date is no longer valid.

I have spent at a few hours looking through the isses that led to the test runtime including the very nice documentation, looked at the source code, issues that led to it. But I could not find out how to do that... I may be missing something obvious...

armanbilge commented 1 year ago

@ybasket answered this on discord:

I think you could just do IO.sleep(durationToWhatYouWantFromEpoch1970) at the beginning of your test as a workaround. Still worth the issue though

We can probably expose some nicer ways to do this.

bblfish commented 1 year ago

Thanks for the tip. It would help I think if the documentation made clear that clock always starts in 1970... Or an example for verifying signatures could make that clear.

I used that trick in the function doAt(time, ioact) of the signature verification suite.

It actually led me to find a timing error in the spec examples. See https://github.com/httpwg/http-extensions/issues/2347

bblfish commented 1 year ago

Mhh this works very differently in the browser than in Java. It works as desired in Java but all the tests fail in JS using this method.

def doAt[A](start: FiniteDuration, act: IO[A]):     IO[Option[Outcome[Id, Throwable, A]]] =
     TestControl.execute(act).flatMap { ctrl =>
       for
          _ <- ctrl.results.assertEquals(None)
          _ <- ctrl.advanceAndTick(start)
          x <- ctrl.results
       yield x
     } 

I guess in the browser TestControl cannot really intercept the IO for the WebCrypto API's async crypto calls. So the result tends to be None even after ctrl.results has been called.

In Java Signing is synchronous, whereas in JS it is async.

So is there another trick to set the time that would work better for the JS platform?

bblfish commented 1 year ago

The obvious workaround to this was given by @SystemFw on Discord here: pass the time as a value to the function doing the testing. So instead of passing the Clock implicity as I used to with the ME below, I mow have the function generated take the time. (Duh!)

      def signatureAuthN[F[_], A](
          fetchKeyId: Rfc8941.SfString => F[SignatureVerifier[F, A]]
      )(
          using ME: MonadError[F, Throwable]
      ): (FiniteDuration, HttpSig) => F[A] = (now, httpSig) =>

(The function takes a function from a keyId to its crypto data, and generates a function that takes signature info and a time to return a verified key - the function is an extension on a request object, so it is given too)

Indeed that simplified the code, clarified things, and the tests now pass. https://github.com/bblfish/httpSig/pull/12

djspiewak commented 1 year ago

I'm going to leave this open to track an enhancement to the TestControl API to make this a bit more obvious and idiomatic. In particular, I'd like to see some functionality on the TestControl class which sets the clock by advancing time by the appropriate amount (which we can allow to be negative), as well as an additional default parameter on executeEmbed (plus the appropriate bincompat shenanigans) which is something like clockStartTime or something like that. Please feel free to bikeshed on teh API.