Skip to content

Commit 562d682

Browse files
authored
Merge pull request #2 from fred1268/release-1.1.1
Release 1.1.1
2 parents 74a4bd0 + a1852c8 commit 562d682

File tree

5 files changed

+95
-28
lines changed

5 files changed

+95
-28
lines changed

README.md

+55-20
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,19 @@ The configuration file looks like the following:
8282

8383
A Server description contains various fields:
8484

85-
- `host`: the URL of the server (including port and everything you don't want to repeat on every test)
86-
- `auth.login`: the information required to login the user, using the same format as a test (see below)
87-
- `auth.session.cookie`: name of the cookie maintaining the session
85+
- `host` (mandatory): the URL of the server (including port and everything you don't want to repeat on every test)
86+
87+
- `auth.login`: used for login/password authentication, using the same format as a test (see below)
88+
89+
- `auth.session.cookie`: used for cookie session management, name of the cookie maintaining the session
90+
91+
- `auth.apikey`: used for API Key authentication, contains both the API Key and the required header
92+
93+
- `auth.session.jwt`: used for JWT session management (`header`, `payload` or `payload.xxx`)
8894

8995
Here `exampleserver1` 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`.
9096

91-
The second server, `exampleserver2` 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"`. You can even use `payload.token` for instance, if your JWT is returned in a `token` field of a JSON object. JWT is always sent back using the `Authorization` header in the form of `Authorization: Bearer my_jwt`.
97+
The second server, `exampleserver2` also uses the `/login` endpoint, but on a different server, hence the endpoint with a different server. 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"`. You can even use `payload.token` for instance, if your JWT is returned in a `token` field of a JSON object. JWT is always sent back using the `Authorization` header in the form of `Authorization: Bearer my_jwt`.
9298

9399
> Please note that in the case of the server definition, `endpoint` must be an fully qualified URL, not a relative endpoint like in the test files. Thus `endpoint` must start with `http://` or `https://`.
94100
@@ -162,26 +168,35 @@ A test file looks like the following:
162168
163169
A test file contains an array of tests, each of them containing:
164170

165-
- `name`: a unique name to globally identify the test (test name must not contain the `. (period)` character)
166-
- `server`: the name of the server used to perform the test (declared in the configuration file)
167-
- `method`: the method to perform the operation (`GET`, `POST`, `PUT`, etc.)
168-
- `endpoint`: the endpoint of the operation (usually a ReST API of some sort)
169-
- `capture`: true if you want to capture the response of this test so that it can be used in another test in this file (fileParallel mode only)
170-
- `skip`: true to have okapi skip this test (useful when debugging a script file)
171-
- `payload`: the payload to be sent to the endpoint (usually with a POST, PUT or PATCH method). This field is optional
171+
- `name` (mandatory): a unique name to globally identify the test (test name must not contain the `. (period)` character)
172+
173+
- `server` (mandatory): the name of the server used to perform the test (declared in the configuration file)
174+
175+
- `method` (mandatory): the method to perform the operation (`GET`, `POST`, `PUT`, etc.)
176+
177+
- `endpoint` (mandatory): the endpoint of the operation (usually a ReST API of some sort)
178+
179+
- `capture` (default false): true if you want to capture the response of this test so that it can be used in another test in this file (fileParallel mode only)
180+
181+
- `skip` (default false): true to have okapi skip this test (useful when debugging a script file)
182+
183+
- `payload` (default none): the payload to be sent to the endpoint (usually with a POST, PUT or PATCH method)
184+
172185
- `expected`: this section contains:
173-
- `statuscode`: the expected status code returned by the endpoint (200, 401, 403, etc.)
174-
- `response`: the expected payload returned by the endpoint. This field is optional
175186

176-
> Please note that `payload` and `response` can be either a string (including json, as shown in 121004), or `@file` (as shown in 121005) or even a `@custom_filename.json` (as shown in doesnotwork). This is useful if you prefer to separate the test from its `payload` or expected `response`. This is particularly handy if the `payload` or `response` are complex JSON structs that you can easily copy and paste from somewhere else, or simply prefer to avoid escaping double quotes.
187+
- `statuscode` (mandatory): the expected status code returned by the endpoint (200, 401, 403, etc.)
188+
189+
- `response` (default none): the expected payload returned by the endpoint. This field is optional
190+
191+
> Please note that `payload` and `response` can be either a string (including json, as shown in 121004), or `@file` (as shown in 121005) or even a `@custom_filename.json` (as shown in doesnotwork). This is useful if you prefer to separate the test from its `payload` or expected `response` (for instance, it is handy if the `payload` or `response` are complex JSON structs that you can easily copy and paste from somewhere else, or simply prefer to avoid escaping double quotes). However, keeping the names for `payload` and `response` like `test_name.payload.json`and `test_name.expected.json` is still a good practice.
177192
178193
> Please also note that `endpoint` and `payload` can use environment variable substitution using the ${env:XXX} syntax (see previous note about environment variable substitution).
179194
180195
> Lastly, please note that `endpoint` and `response` can use captured variable (i.e. variables inside a captured response, see `"capture":true`). For instance, to use the `id` field returned inside of a `user` object in test `mytest`, you will use `${mytest.user.id}`. In the example above, we used `${cap121004.id}` to retrieve the ID of the returned response in test `cap121004`. Captured response also works with arrays.
181196
182197
### Payload and Response files
183198

184-
Payload and response files don't have a specific format, since they represent whatever the server you are testing is expecting from or returns to you. The only important things to know about the payload and response files, is that they must be placed in the test directory, and must be named `<name_of_test>.payload.json` and `<name_of_test>.expected.json` (`121005.expected.json` in the example above) respectively if you specify `@file`. If you decide to use a custom filename for your `payload` and/or `response`, then you can specify the name of your choice prefixed by `@` (`@custom_filename.json` in the example above).
199+
Payload and response files don't have a specific format, since they represent whatever the server you are testing is expecting from or returns to you. The only important things to know about the payload and response files, is that they must be placed in the test directory, and must be named `<name_of_test>.payload.json` and `<name_of_test>.expected.json` (`121005.expected.json` in the example above) respectively if you specify `@file`. Alternatively, they can also be put in a `payload/` or `expected/` subdirectory of the test directory, and, in that case, be named `<name_of_test>.json`. If you decide to use a custom filename for your `payload` and/or `response`, then you can specify the name of your choice prefixed by `@` (`@custom_filename.json` in the example above).
185200

186201
## Expected response
187202

@@ -194,6 +209,8 @@ As we saw earlier, for each test, you will have to define the expected response.
194209
- if the response is a non-JSON string:
195210
- the response is compared to `expected` and success or failure is reported
196211

212+
> Please note that, in the case of non-JSON responses, you can use the `%` character to match start, end or part of the response, pretty much like in SQL. For instance, expected responses like `"%test"`, `"test%"` and `"%test%"` will match test at the beginning, end or as part of the returned response respectively.
213+
197214
## Running okapi :giraffe:
198215

199216
To launch okapi, please run the following:
@@ -205,23 +222,40 @@ To launch okapi, please run the following:
205222
where options are one or more of the following:
206223

207224
- `--servers-file`, `-s` (mandatory): point to the configuration file's location
208-
- `--timeout` (default 30s): set a default timeout for all HTTP requests
225+
209226
- `--verbose`, `-v` (default no): enable verbose mode
210-
- `--no-parallel` (default parallel): prevent tests from running in parallel
227+
211228
- `--file-parallel` (default no): run the test files in parallel (instead of the tests themselves)
229+
230+
- `--file` (default none): only run the specified test file
231+
232+
- `--test` (default none): only run the specified standalone test
233+
234+
- `--timeout` (default 30s): set a default timeout for all HTTP requests
235+
236+
- `--no-parallel` (default parallel): prevent tests from running in parallel
237+
212238
- `--user-agent` (default okapi UA): set the default user agent
239+
213240
- `--content-type` (default application/json): set the default content type for requests
241+
214242
- `--accept` (default application/json): set the default accept header for responses
215-
- `test_directory`: point to the directory where all the test files are located
243+
244+
- `test_directory` (mandatory): point to the directory where all the test files are located
216245

217246
> Please note that the `--file-parallel` mode is particularly handy if you want to have a sequence of tests that needs to run in a specific order. For instance, you may want to create a resource, update it, and delete it. Placing these three tests in the same file and in the right order, and then running okapi with `--file-parallel` should do the trick. The default mode is used for unit tests, whereas the `--file-parallel` mode is used for (complex) test scenarios.
218247
219248
## Output example
220249

221-
If you run okapi in verbose mode with the HackerNews API tests, you should get the following outout:
250+
To try the included examples (located in `./assets/tests`), you need to run the following command:
222251

223252
```shell
224253
$ okapi --servers-file ./assets/config.json --verbose ./assets/tests
254+
```
255+
256+
You should then see the following output:
257+
258+
```shell
225259
--- PASS: hackernews.items.test.json
226260
--- PASS: 121014 (0.35s)
227261
--- PASS: 121012 (0.36s)
@@ -246,13 +280,14 @@ ok hackernews.items.test.json 0.363s
246280
PASS
247281
ok hackernews.users.test.json 0.368s
248282
okapi total run time: 0.368s
249-
$
250283
```
251284

252285
## Integrating okapi :giraffe: with your own software
253286

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

289+
You can find more information about the exposed API on [The Official Go Package](https://pkg.go.dev/github.com/fred1268/okapi/testing).
290+
256291
## Feedback and contribution
257292

258293
Feel free to send feedback, PR, issues, etc.

testing/config.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import "github.com/fred1268/go-clap/clap"
55
// Config holds okapi's configuration.
66
type Config struct {
77
Servers string `clap:"--servers-file,-s,mandatory"`
8-
Tests string `clap:"trailing"`
8+
Directory string `clap:"trailing"`
99
Timeout int `clap:"--timeout"`
1010
UserAgent string `clap:"--user-agent"`
1111
ContentType string `clap:"--content-type"`
1212
Accept string `clap:"--accept"`
1313
Verbose bool `clap:"--verbose,-v"`
1414
Parallel bool `clap:"--parallel,-p"`
1515
FileParallel bool `clap:"--file-parallel"`
16+
File string `clap:"--file,-f"`
17+
Test string `clap:"--test,-t"`
1618
}
1719

1820
// LoadConfig returns okapi's configuration from the

testing/internal/json/json.go

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func compareSlices(src, dst []any) error {
4646
}
4747
}
4848
found++
49+
break
4950
}
5051
}
5152
if found != len(src) {

testing/loader.go

+35-6
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ func readJSONDependencies(directory string, requests []*APIRequest) error {
2424
}
2525
content, err := os.ReadFile(path.Join(directory, file))
2626
if err != nil {
27-
return fmt.Errorf("cannot read test file '%s': %w", file, err)
27+
file = fmt.Sprintf("payload/%s.json", strings.ToLower(request.Name))
28+
content, err = os.ReadFile(path.Join(directory, file))
29+
if err != nil {
30+
return fmt.Errorf("cannot read test file '%s': %w", file, err)
31+
}
2832
}
2933
request.Payload = string(content)
3034
}
@@ -35,7 +39,11 @@ func readJSONDependencies(directory string, requests []*APIRequest) error {
3539
}
3640
content, err := os.ReadFile(path.Join(directory, file))
3741
if err != nil {
38-
return fmt.Errorf("cannot read test file '%s': %w", file, err)
42+
file = fmt.Sprintf("expected/%s.json", strings.ToLower(request.Name))
43+
content, err = os.ReadFile(path.Join(directory, file))
44+
if err != nil {
45+
return fmt.Errorf("cannot read test file '%s': %w", file, err)
46+
}
3947
}
4048
request.Expected.Response = string(content)
4149
}
@@ -48,8 +56,8 @@ func readJSONDependencies(directory string, requests []*APIRequest) error {
4856
//
4957
// The result is a map indexed by the file name, its value being an
5058
// array of *APIRequests corresponding to the tests in the file.
51-
func LoadTests(directory string) (map[string][]*APIRequest, error) {
52-
files, err := os.ReadDir(directory)
59+
func LoadTests(cfg *Config) (map[string][]*APIRequest, error) {
60+
files, err := os.ReadDir(cfg.Directory)
5361
if err != nil {
5462
return nil, err
5563
}
@@ -58,7 +66,10 @@ func LoadTests(directory string) (map[string][]*APIRequest, error) {
5866
if !strings.HasSuffix(file.Name(), ".test.json") {
5967
continue
6068
}
61-
content, err := os.ReadFile(path.Join(directory, file.Name()))
69+
if cfg.File != "" && cfg.File != file.Name() {
70+
continue
71+
}
72+
content, err := os.ReadFile(path.Join(cfg.Directory, file.Name()))
6273
if err != nil {
6374
return nil, fmt.Errorf("cannot read test file '%s': %w", file.Name(), err)
6475
}
@@ -68,9 +79,27 @@ func LoadTests(directory string) (map[string][]*APIRequest, error) {
6879
if err = json.NewDecoder(bytes.NewReader(content)).Decode(&tests); err != nil {
6980
return nil, fmt.Errorf("cannot decode json file '%s': %w", file.Name(), err)
7081
}
71-
if err := readJSONDependencies(directory, tests.Tests); err != nil {
82+
if err := readJSONDependencies(cfg.Directory, tests.Tests); err != nil {
7283
return nil, err
7384
}
85+
if len(tests.Tests) == 0 {
86+
log.Printf("Skipping '%s': no tests found in file\n", file.Name())
87+
continue
88+
}
89+
if cfg.Test != "" {
90+
var test *APIRequest
91+
for _, t := range tests.Tests {
92+
if cfg.Test == t.Name {
93+
test = t
94+
break
95+
}
96+
}
97+
if test != nil {
98+
allTests[file.Name()] = []*APIRequest{test}
99+
break
100+
}
101+
continue
102+
}
74103
allTests[file.Name()] = tests.Tests
75104
}
76105
return allTests, nil

testing/test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func Run(ctx context.Context, cfg *Config) error {
134134
if err != nil {
135135
return fmt.Errorf("cannot connect to servers: %w", err)
136136
}
137-
allTests, err := LoadTests(cfg.Tests)
137+
allTests, err := LoadTests(cfg)
138138
if err != nil {
139139
return fmt.Errorf("cannot read tests: %w", err)
140140
}

0 commit comments

Comments
 (0)