### ----------------------------------------------------
### -- cro
### -- Licenses: Artistic-2.0
### -- Authors: Jonathan Worthington <jnthn@jnthn.net>
### -- File: docs/reference/cro-http-test.md
### ----------------------------------------------------
# Cro::HTTP::Test

Tests for a Cro HTTP service can in principle be written by hosting the
application using `Cro::HTTP::Server`, using `Cro::HTTP::Client` to make
requests to it, and using the standard `Test` library to check the results.
This library makes writing such tests easier, and executing them faster, by:

* Providing a more convenient API for making test requests and checking the
  results
* Skipping the network and just passing `Cro::TCP` objects from the client
  pipeline to the server pipeline and vice versa

## A basic example

Given a module `MyService::Routes` that looks like:

```
sub routes() is export {
    route {
        get -> {
            content 'text/plain', 'Nothing to see here';
        }
        post -> 'add' {
            request-body 'application-json' => -> (:$x!, :$y!) {
                content 'application/json', { :result($x + $y) };
            }
        }
    }
}
```

We could write tests for it like this:

```
use Cro::HTTP::Test;
use MyService::Routes;

test-service routes(), {
    test get('/'),
        status => 200,
        content-type => 'text/plain',
        body => /nothing/;

    test-given '/add', {
        test post(json => { :x(37), :y(5) }),
            status => 200,
            json => { :result(42) };

        test post(json => { :x(37) }),
            status => 400;

        test get(json => { :x(37) }),
            status => 405;
    }
}

done-testing;
```

## Setting the service to test

The `test-service` function has two candidates.

The `test-service(Cro::Transform, &tests, :$fake-auth, :$http)` candidate
runs the tests against the HTTP application provided, which can be any
`Cro::Transform` that consumes a `Cro::HTTP::Request` and produces a
`Cro::HTTP::Response`. Applications that are written with `Cro::HTTP::Router`
do this. It is also possible to use `Cro.compose` to put (potentially mock)
middleware in place also. The optional `:$fake-auth` parameter, if passed,
will prepend a middleware that sets the `auth` of the request to the
specified object. This is useful for simulating a user or session and
thus testing authorization. The `http` argument specifies the HTTP version to
run the tests under. Since we control both client and server side in the test,
a setting of `:http<1.1 2>` is not allowed. The default is `:http<2>`. It is
also possible to fake the `peer-host` and `peer-port` by passing these named
arguments to `test-service`.

The `test-service($uri, &tests)` candidate runs the tests against the specified
base URI, connecting to it through `Cro::HTTP::Client`. This makes it possible
to use `Cro::HTTP::Test` to write tests for services built using something other
than Cro.

All other named parameters are passed as `Cro::HTTP::Client` constructor
arguments.

## Writing tests

The `test` function is for use inside of the block passed to `test-service`.
It expects to be passed one positional argument representing the request to
test, and named parameters indicating the expected properties of the response.

The request is specified by calling one of `get`, `put`, `post`, `delete`,
`head`, or `patch`. There's also `request($method, ...)` for other HTTP methods
(in fact, `get` will just call `request('GET', ...)`). These functions accept
an optional positional parameter providing a relative URI, which if provided
will be appended to the current effective base URI. The `:$json` named parameter
is treated specially, expanding to `{ content-type => 'application/json', body
=> $json)`. All other named parameters will be passed on to the `Cro::HTTP::Client`
`request` method, thus making all of the HTTP client's functionality available.

Named parameters to the `test` function constitute checks. They largely follow
the names of methods on the `Cro::HTTP::Response` object. The available checks
are as follows.

### status

Smartmatches the `status` property of the response against the
check. While an integer, such as `status => 200`, will be most common, it is
also possible to so things like `status => * < 400` (e.g. not an error).

### content-type

Checks the content-type is equivalent. If passed a string,  it parses it as a
media type and checks the type and subtype match that of the response. If
there are any extra parameters in the string (such as a charset), then these
will be checked for in the received media type also. If the received media type
has extra parameters that are not mentioned, then these will be disregarded.
Thus a check `content-type => 'text/plain'` matches `text/plain; charset=utf8`
in the response.

For more fine-grained control, pass a block, which will be passed an instance
of `Cro::MediaType` and expected to return someting truthy for the test to
pass.

### header or headers

Takes either a hash mapping header names to header values, or a list of `Pair`
doing the same. The test passes if the headers are present and the value of 
the header smartmatches against the value. Use `*` when only caring that the
header exists, but not wishing to check its values. All other headers in the
response will be ignored (that is, extra headers are considered fine).

```
    headers => {
        Strict-Transport-Security => *,
        Cache-Control => /public/
    }
```

For further control, pass a block, which will receive a `List` of `Pair`, each
one representing a header. Its return value should the truthy for the test to
pass.

### body-text

Obtains the `body-text` of the response and smart-matches it against the
provided value. A string, regex, or code object are all potentially useful.

```
    body-text => /:i success/
```

The body test will be skipped if there is a `content-type` tested and that
test fails.

### body-blob

Obtains the `body-blob` of the response and smart-matches it against the
provided value.

```
    body-blob => *.bytes > 128
```

The body test will be skipped if there is a `content-type` tested and that
test fails.

### body

Obtains the `body` of the response and smart-matches it against the provided
value. Note that the `body` property decides what to produce based on the
`content-type` of the response, thus picking an appropriate body parser. It
is thus recommended to use this together with `content-type` (that will always
be tested ahead of `body`, and the `body` test skipped if it fails).

### json

This is a convenience short-cut for the common case of a JSON response. It
imples `content-type => { .type eq 'application' && .subtype-name eq 'json'
|| .suffix eq 'json' }` (that is, it accepts `application/json` or something
like `application/vnd.foobar+json`).

If passed a code value, then the code will be invoked with the deserialized
JSON body and should return a truthy value for the test to pass. Otherwise,
the `is-deeply` test routine will be used to check the structure of the JSON
that was received matches what was expected.

## Many tests with one URI, set of headers, etc.

It can get tedious to repeat the same details of a test. For example, it is
common to wish to write many tests against the same URI, passing it a
different body or using different request methods each time. The `test-given`
function comes in various forms. It can be used with a URI and a block:

```
test-given '/add', {
    test post(json => { :x(37), :y(5) }),
        status => 200,
        json => { :result(42) };
    test post(json => { :x(37) }),
        status => 400;
}
```

In this case, the tests will all be performed against this URI appended to
the current effective URI, which in a `test-service` block is the base URI of
the service being tested. If the individual `test` cases have a URI too, it
will also be appended. It is possible to nest `test-given` blocks, and each
appends its URI segments, establishing a new current effective URI.

It is also possible to pass named parameters to `test-given`, and these will
be used as request parameters, passed along to `Cro::HTTP::Client`. Note that
any named parameters that are specified to `get` or `request` will override
those specified in `test-given`.

```
test-given '/add', headers => { X-Precision => '15' } {
    ...
}
```

The second form doesn't require a relative URI, and instead just takes options:

```
test-given headers => { X-Precision => '15' } {
    ...
}
```


