diff --git a/README.md b/README.md index ecec93a..46630e5 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ A CLI api tester and request builder, totally not born out of frustration from s ## Why I'd much rather edit my test routes in my text editor as bare config files and fire them via a CLI than fumble around some GUI application. +As an example, most API test tools have features to automatically split down query arguments from a built url. PostWoman doesn't bother, as your editor will most likely offer multi-cursors and search/replace to easily split off query parameters. + +While PostWoman will never be as fully featured as other graphical tools, it doesn't need to be to provide a solid API testing framework. + # Usage `postwoman` expects a `postwoman.toml` collection in your cwd. A different file or path can be specified with the global `-c` option. @@ -20,22 +24,133 @@ url = "https://api.alemi.dev/debug" But more complex options are available ```toml -[client] -user_agent = "api-tester@alemi.dev" +[client] # HTTP client configuration +user_agent = "postwoman@sample/0.2.0" + +[env] # these will be replaced in routes options. environment vars overrule these +PW_TOKEN = "set-me-as-and-environment-variable!" + + + +[route.healthcheck] # the simplest possible route: just name and url +url = "https://api.alemi.dev/" [route.debug] url = "https://api.alemi.dev/debug" -method = "PUT" -headers = ["Content-Type: application/json"] -body = { hello = "world!", success = true } -extract = { type = "body" } +method = "PUT" # specify request method +query = [ # specify query parameters in a more friendly way + "body=json", + "cache=0" +] +headers = [ # add custom headers to request + "Content-Type: application/json", + "Authorization: Bearer ${PW_TOKEN}", +] +body = { hello = "world!", success = true } # body can be a bare string, or an inline table (will be converted to json) +extract = ".path" # extract from json responses with JQ syntax +# note that a bare extractor string is equivalent to `{ type = "jq", query = ".path" }` + +[route.benchmark] +url = "https://api.alemi.dev/look/into/the/void" +extract = { type = "discard" } # if you don't care about the output, discard it! + +[route.notfound] +url = "https://api.alemi.dev/not-found" +expect = 404 # it's possible to specify expected status code, will fail if doesn't match +extract = { type = "regex", pattern = 'nginx/[0-9\.]+' } # extract from response with regex + +[route.payload] +url = "https://api.alemi.dev/debug" +method = "POST" +body = '''{ + "complex": { + "json": "payloads", + "can": "be", + "expressed": "this", + "way": true + } +}''' +extract = { type = "body" } # get the whole response body, this is the default extractor [route.cookie] url = "https://api.alemi.dev/getcookie" method = "GET" -headers = [ - "Authorization: Basic ...", - "Accept: application/json" -] -extract = { type = "header", key = "Set-Cookie" } +extract = { type = "header", key = "Set-Cookie" } # get a specific response header, ignoring body +``` + +### Running +Show collection summary +``` +$ postwoman +> postwoman@sample/0.2.0 ++ PW_TOKEN: set-me-as-and-environment-variable! + +- healthcheck: GET https://api.alemi.dev/ +- debug: PUT https://api.alemi.dev/debug +- benchmark: GET https://api.alemi.dev/look/into/the/void +- notfound: GET https://api.alemi.dev/not-found +- payload: POST https://api.alemi.dev/debug +- cookie: GET https://api.alemi.dev/getcookie +``` + +Run all endpoints matching `.` (aka all of them) +``` +$ postwoman run . + : [22:20:53.112717] sending healthcheck ... + + [22:20:53.250755] healthcheck done in 138ms +Response { + url: "https://api.alemi.dev/", + status: 200, + headers: { + "server": "nginx/1.26.2", + "date": "Sat, 19 Oct 2024 20:20:53 GMT", + "content-type": "application/json", + "content-length": "161", + "connection": "keep-alive", + "vary": "Accept-Encoding", + "access-control-allow-origin": "*", + }, +} +Body: { + "example": [ + "https://api.alemi.dev/debug", + "https://api.alemi.dev/msg", + "https://api.alemi.dev/mumble/ping" + ], + "time": "Saturday, 19-Oct-2024 20:20:53 GMT", + "up": true +} + + : [22:20:53.250802] sending debug ... + + [22:20:53.369298] debug done in 118ms +/debug?body=json&cache=0 + : [22:20:53.369352] sending benchmark ... + + [22:20:53.482720] benchmark done in 113ms + : [22:20:53.482745] sending notfound ... + + [22:20:53.593134] notfound done in 110ms +nginx/1.26.2 + : [22:20:53.593163] sending payload ... + + [22:20:53.709245] payload done in 116ms +{ + "body": "{\n\t\"complex\": {\n\t\t\"json\": \"payloads\",\n\t\t\"can\": \"be\",\n\t\t\"expressed\": \"this\",\n\t\t\"way\": true\n\t}\n}", + "headers": { + "accept": [ + "*/*" + ], + "connection": "close", + "content-length": "94", + "user-agent": "postwoman@sample/0.2.0", + "x-forwarded-proto": "https", + "x-real-ip": "93.34.149.115", + "x-real-port": 46437, + "x-user-agent": "postwoman@sample/0.2.0" + }, + "method": "POST", + "path": "/debug", + "time": 1729369253.6897264, + "version": "HTTP/1.0" +} + : [22:20:53.709276] sending cookie ... + + [22:20:53.822684] cookie done in 113ms +SGF2ZSBhIENvb2tpZSE= ``` diff --git a/postwoman.toml b/postwoman.toml index 265c926..7837f96 100644 --- a/postwoman.toml +++ b/postwoman.toml @@ -1,34 +1,37 @@ -[client] +[client] # HTTP client configuration user_agent = "postwoman@sample/0.2.0" -[env] +[env] # these will be replaced in routes options. environment vars overrule these PW_TOKEN = "set-me-as-and-environment-variable!" -[route.healthcheck] + + +[route.healthcheck] # the simplest possible route: just name and url url = "https://api.alemi.dev/" -[route.benchmark] -url = "https://api.alemi.dev/look/into/the/void" -extract = { type = "discard" } - -[route.notfound] -url = "https://api.alemi.dev/not-found" -expect = 404 -extract = { type = "regex", pattern = 'nginx/[0-9\.]+' } - [route.debug] url = "https://api.alemi.dev/debug" -method = "PUT" -headers = [ - "Content-Type: application/json", - "Authorization: Bearer ${PW_TOKEN}", -] -query = [ +method = "PUT" # specify request method +query = [ # specify query parameters in a more friendly way "body=json", "cache=0" ] -body = { hello = "world!", success = true } -extract = { type = "jql", query = ".path" } +headers = [ # add custom headers to request + "Content-Type: application/json", + "Authorization: Bearer ${PW_TOKEN}", +] +body = { hello = "world!", success = true } # body can be a bare string, or an inline table (will be converted to json) +extract = ".path" # extract from json responses with JQ syntax +# note that a bare extractor string is equivalent to `{ type = "jq", query = ".path" }` + +[route.benchmark] +url = "https://api.alemi.dev/look/into/the/void" +extract = { type = "discard" } # if you don't care about the output, discard it! + +[route.notfound] +url = "https://api.alemi.dev/not-found" +expect = 404 # it's possible to specify expected status code, will fail if doesn't match +extract = { type = "regex", pattern = 'nginx/[0-9\.]+' } # extract from response with regex [route.payload] url = "https://api.alemi.dev/debug" @@ -41,9 +44,9 @@ body = '''{ "way": true } }''' -extract = { type = "body" } +extract = { type = "body" } # get the whole response body, this is the default extractor [route.cookie] url = "https://api.alemi.dev/getcookie" method = "GET" -extract = { type = "header", key = "Set-Cookie" } +extract = { type = "header", key = "Set-Cookie" } # get a specific response header, ignoring body diff --git a/src/main.rs b/src/main.rs index 0a61505..605250e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,7 @@ struct PostWomanArgs { pub enum PostWomanActions { /// execute specific endpoint requests Run { - /// regex query filter, run all with '.*' + /// regex query filter, run all with '.' query: String, /// run requests in parallel diff --git a/src/model/endpoint.rs b/src/model/endpoint.rs index a1ba1ec..2012a97 100644 --- a/src/model/endpoint.rs +++ b/src/model/endpoint.rs @@ -169,7 +169,7 @@ impl Endpoint { + "\n" }, // bare string defaults to JQL query - StringOr::T(Extractor::Jql { query }) | StringOr::Str(query) => { + StringOr::T(Extractor::JQ { query }) | StringOr::Str(query) => { let json: serde_json::Value = res.json().await?; let selection = jq(&query, json)?; if selection.len() == 1 { diff --git a/src/model/extractor.rs b/src/model/extractor.rs index c46c209..d380dcd 100644 --- a/src/model/extractor.rs +++ b/src/model/extractor.rs @@ -6,7 +6,7 @@ pub enum Extractor { Debug, Body, Discard, - Jql { query: String }, + JQ { query: String }, Regex { pattern: String }, Header { key: String }, }