Skip to content

Commit

Permalink
First release
Browse files Browse the repository at this point in the history
  • Loading branch information
Fred committed Jul 13, 2023
1 parent 0d9e0aa commit 3ec61a1
Show file tree
Hide file tree
Showing 18 changed files with 1,007 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@

# Go workspace file
go.work
.devcontainer
.vscode
191 changes: 190 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,191 @@
# okapi
# okapi :giraffe:

API tests made as easy as table driven tests.

## Introduction

okapi is a program allowing you to test your APIs by using tests files and test cases, pretty much like the `go test` command with table-driven tests. okapi will iterate on all `.test.json` files in the specified directory and runs every test case containted within the files, sequentially or in parallel.

The result of each test case is then compared to the expected result (both the HTTP Response Status Code, as well as the payload). Success or failure are reported.

## Setup

Pretty easy to install:

```shell
go install github.com/fred1268/okapi@latest
```

> Also, note that okapi uses [**clap :clap:, the Command Line Argument Parser**](https://github.com/fred1268/go-clap). clap is a non intrusive, lightweight command line parsing library you may want to try out in your own projects. Feel free to give it a try!
## Configuring okapi :giraffe:

In order to run, okapi requires at least two files:

- a configuration file
- one or more test files
- zero or more result files

### Configuration file

The configuration file looks like the following:

```json
{
"server1": {
"host": "http://localhost:8080",
"auth": {
"login": {
"method": "POST",
"endpoint": "/login",
"payload": "{\"email\":\"[email protected]\",\"password\":\"my_password\"}",
"expected": {
"statuscode": 200
}
}
},
"session": {
"cookie": {
"name": "jsessionid"
}
}
},
"server2": {
"host": "http://localhost:9090",
"auth": {
"login": {
"method": "POST",
"endpoint": "http://localhost:8080/login",
"payload": "{\"email\":\"[email protected]\",\"password\":\"my_password\"}",
"expected": {
"statuscode": 200
}
}
},
"session": {
"jwt": "header"
}
},
"server3": {
"host": "http://localhost:8088",
"auth": {
"apikey": {
"apikey": "Bearer: my_key",
"header": "Authorization"
}
}
},
"hackernews": {
"host": "https://hacker-news.firebaseio.com"
}
}
```

A Server description contains various fields:

- `host`: the URL of the server (including port and everything you don't want to repeat on every test)
- `auth.login`: the information required to login the user, using the same format as a test (see below)
- `session.cookie`: name of the cookie maintaining the session

Here `server1` uses the `/login` endpoint on the same HTTP server than the one used for the tests. Both `email` and `password` are submitted in the `POST`, and `200 OK` is expected upon successful login. The session is maintained by a session cookie called `jsessionid`.

The second server, `server2` also uses the `/login` endpoint, but on a different server, hence the fully qualified URL given as the endpoint. The sesssion is maintained using a JWT (JSON Web Token) which is obtained though a header (namely `Authorization`). Should your JWT be returned as a payload, you can specify `"payload"` instead of `"header"`. JWT is always sent back using the `Authorization` header in the form of `Authorization: Bearer my_jwt`.

The third server, `server3` uses API Keys for authentication. The apikey field contains the key itself, whereas the header field contains the field used to send the API Key back (usually `Authorization`). Please note that session is not maintained in this example, since the API Key is sent with each request.

The last server, `hackernews`, is a server which doesn't require any authentication.

### Test files

A test file looks like the following:

```json
{
"tests": [
{
"name": "121003",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121003.json",
"expected": {
"statuscode": 200
}
},
{
"name": "121004",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121004.json",
"expected": {
"statuscode": 200,
"result": "{\"id\":121004}"
}
},
{
"name": "121005",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121005.json",
"expected": {
"statuscode": 200,
"result": "@file"
}
}
]
}
```

> The test files must end with `.test.json` in order for okapi to find them. A good pratice is to name them based on your routes. For example, in this case, since we are testing hackernews' `item` route, the file could be named `item.test.json` or `item.get.test.json` if you need to be more specific.
A test file contains an array of tests, each of them containing:

- `name`: a unique name to globally identify the test
- `server`: the name of the server used to perform the test (declared in the configuration file)
- `method`: the method to perform the operation (`GET`, `POST`, `PUT`, etc.)
- `endpoint`: the endpoint of the operation (usually a ReST API of some sort)
- `expected`: this section contains:
- `statuscode`: the expected status code returned by the endpoint (200, 401, 403, etc.)
- `result`: the expected payload returned by the endpoint. This field is optional.

> Please note that result can be either a string (including json, like shown on 121004), or a `@file` (like in 121005) if you prefer to separate the test from its expected result. This can be handy if the result are complex JSON structs that you can easily copy and paste from somewhere else, or simply want to avoid escaping double quotes.
### Result file

A result file doesn't have a specific format, since it represents whatever the server you are testing returns. The only important thing about the result file, is that it must be named `<name>.expected.json` within the current directory, where `name` is the name of the current test.

## Expected result

As we saw earlier, for each test, you will have to define the expected result. okapi will always compare the HTTP Response Status Code with the one provided, and can optionally, compare the returned payload. The way it works is pretty simple:

- if the result is in JSON format:
- if a field is present in expected, okapi will also check for its presence in the result
- if the result contains other fields not mentioned in `expected`, they will be ignored
- if the result is a non-JSON string:
- the result is compared to `expected` and success or failure is reported

## Running okapi :giraffe:

To launch okapi, please run the following:

```shell
okapi <options> test_directory
```

where options are one or more of the following:

- `test_directory`: the directory where all the test files are located
- `--servers-file` or `-s` (mandatory): allows to point to the configuration file
- `--timeout` (default 30s): allows to set a default timeout for all HTTP requests
- `--verbose` (`--no-verbose`) or `-v` (default no): verbose mode
- `--parallel` (`--no-parallel`) or `-p` (default yes): run tests in parallel
- `--user-agent` (default okapi ua): to set the default user agent
- `--content-type` (default application/json): to set the default content type for requests
- `--accept` (default application/json): to set the default accept header for responses

## Integration okapi :giraffe: with your own software

okapi exposes a pretty simple and straightforward API that you can use within your own Go programs.

## Feedback

Feel free to send feedback, star, issues, etc.
5 changes: 5 additions & 0 deletions assets/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"hackernews": {
"host": "https://hacker-news.firebaseio.com"
}
}
11 changes: 11 additions & 0 deletions assets/tests/121005.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"by": "paulgb",
"id": 121005,
"kids": [
121021
],
"parent": 120890,
"text": "I wish I had Mario. Instead I was stuck with Mavis Beacon.",
"time": 1203647923,
"type": "comment"
}
114 changes: 114 additions & 0 deletions assets/tests/hackernews.items.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{
"tests": [
{
"name": "121003",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121003.json",
"expected": {
"statuscode": 200
}
},
{
"name": "121004",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121004.json",
"expected": {
"statuscode": 200,
"result": "{\"id\":121004}"
}
},
{
"name": "121005",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121005.json",
"expected": {
"statuscode": 200,
"result": "@file"
}
},
{
"name": "121006",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121006.json",
"expected": {
"statuscode": 200
}
},
{
"name": "121007",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121007.json",
"expected": {
"statuscode": 200
}
},
{
"name": "121008",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121008.json",
"expected": {
"statuscode": 200
}
},
{
"name": "121009",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121009.json",
"expected": {
"statuscode": 200
}
},
{
"name": "121010",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121010.json",
"expected": {
"statuscode": 200
}
},
{
"name": "121011",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121011.json",
"expected": {
"statuscode": 200
}
},
{
"name": "121012",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121012.json",
"expected": {
"statuscode": 200
}
},
{
"name": "121013",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121013.json",
"expected": {
"statuscode": 200
}
},
{
"name": "121014",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/item/121014.json",
"expected": {
"statuscode": 200
}
}
]
}
49 changes: 49 additions & 0 deletions assets/tests/hackernews.users.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"tests": [
{
"name": "jk",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/user/jk.json",
"expected": {
"statuscode": 200
}
},
{
"name": "jl",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/user/jl.json",
"expected": {
"statuscode": 200
}
},
{
"name": "jc",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/user/jc.json",
"expected": {
"statuscode": 200
}
},
{
"name": "jo",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/user/jo.json",
"expected": {
"statuscode": 200
}
},
{
"name": "401",
"server": "hackernews",
"method": "GET",
"endpoint": "/v0/users/jl.json",
"expected": {
"statuscode": 401
}
}
]
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/fred1268/okapi

go 1.20

require github.com/fred1268/go-clap v1.1.0
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/fred1268/go-clap v1.1.0 h1:bxgST/3OvEls2mzkZaxbX5322x7bnSL5VnxAnYQNxIs=
github.com/fred1268/go-clap v1.1.0/go.mod h1:A5/yYBapOy6UyujlbxL7p/bX9J7bzyoMRzQKFwveXF0=
Loading

0 comments on commit 3ec61a1

Please sign in to comment.