Testing forms with Dream web framework

I’ve been exploring the Dream web framework. It’s a really good introduction to OCaml with easily understandable public types and plenty of examples. Thanks aantron and everyone else who has contributed.

It has built-in CSRF protection for forms which is great, but generating the csrf-code seems to need you to be in a response context (where you would normally be generating your form html). This is not the case when you are testing your Dream site/app though.

I’ve come up with the following to allow me to test forms and wondered if anyone with more experience than myself has a better approach. If not, hopefully this will be searchable for later learners. Oh - at time of writing it is March 2023 and Dream is at 1.0.0 alpha

I have a handlers.ml with what is presumably the typical one function for each page-handler:

type handler_settings = {
  mutable csrf_checking : bool;
}

let settings = { csrf_checking=true }

let app_form req = Dream.form ~csrf:settings.csrf_checking req

(* Below I replaced `Dream.form` with `app_form` *)
let handle_login req =
  match%lwt app_form req with
  | `Ok [ "password", p; "username", u ] ->
      Dream.log "user %s / pass = %s" u p;
      ...

Page handlers need to remember to call app_form in place of Dream.form but otherwise no changes are needed.

Then in the test file we need to make sure CSRF is turned off:

module H = Rota_app.Handlers

let tests = "login", [
  Alcotest.test_case "login-successful" `Quick begin fun() ->
    H.settings.csrf_checking <- false;
    let login_fields = [ "username", "me"; "password", "pass" ] in
    let req = make_form_request login_fields in
    let handler = H.handle_login in
    let res = req |>
        Dream.test
        @@ Dream.memory_sessions
        @@ Dream.flash
        @@ handler in
        is_redirect_to res "/";
  end;
]

The make_form_request and is_redirect_to are helper functions that do what you would expect.

I could probably have done something via making Handlers a first-class module or functor or some such but that felt over-engineered for what is currently just one flag for testing. Maybe I’m wrong though - anyone have suggestions?

Hello,

When you post the post, Dream does not match if the given crsf string is the same as the one used in the form. It just match if the crsf token is valid. So, as long as you provide a valid token, the test will pass.

You can create directly the crsf token string with the function crsf_token and Dream expect a field name dream.csrf in the form, so you can forge it completely inside your test.

I went back and tried csrf_token again to see if I was missing something, but got stuck at the same problem. The docs say

The call must be done under a session middleware, since each CSRF token is scoped to a session

This makes sense and indeed matches the error I get

dream.session ERROR Missing session middleware

This is from where I am building my request and assembling my form fields which is before I’ve got any handlers for my session middleware

let token = Dream.csrf_token req in
...
Dream.set_body req login_form_body;
...
req |> Dream.test @@ Dream.memory_sessions @@ Dream.flash @@ handler

To create the token I seem to need to generate my request within the context of the session handler.

I’ve made this simple test with alcotest in order to see how to check the token:

let csrf_result : Dream.csrf_result Alcotest.testable =
  Alcotest.testable
    (fun formatter -> function
      | `Ok -> Format.fprintf formatter "Ok"
      | `Expired _ -> Format.fprintf formatter "Expired"
      | `Wrong_session -> Format.fprintf formatter "Wrong session"
      | `Invalid -> Format.fprintf formatter "Invalid")
    Stdlib.( = )

let test_csrf switch () =
  ignore switch;

  let request = Dream.request ~method_:`GET ~target:"/" "" in
  let handler = Dream.memory_sessions Dream.echo in

  (* Run the request through the handler, Dream will be ready 
     for the csrf token after that *)
  let%lwt _ = handler request in

  let token = Dream.csrf_token request in

  let%lwt check = Dream.verify_csrf_token request token in
  Alcotest.check' ~msg:"Check the csrf result" csrf_result ~expected:`Ok
    ~actual:check;
  Lwt.return_unit

let tests =
  let open Alcotest_lwt in
  ("csrf", [ test_case "csrf" `Quick test_csrf ])

This works fine. Here I’m using the same request, but you should be able to change the setup-up in order to check against your form response.