diff --git a/README.md b/README.md index c0f880f16..a253ebaf2 100755 --- a/README.md +++ b/README.md @@ -40,11 +40,11 @@ And you don't need to create Java objects (or POJO-s) for any of the payloads th ----- | ---- | ---- | --- | --- **Getting Started** | [Maven](#maven) | [Folder Structure](#recommended-folder-structure) | [File Extension](#file-extension) | [Running with JUnit](#running-with-junit) | [Cucumber Options](#cucumber-options) | [Command Line](#command-line) | [Logging](#logging) | [Configuration](#configuration) - | [Environment Switching](#switching-the-environment) | [Script Structure](#script-structure) | [Given-When-Then](#given-when-then) + | [Environment Switching](#switching-the-environment) | [Script Structure](#script-structure) | [Given-When-Then](#given-when-then) | [Cucumber vs Karate](#cucumber-vs-karate) **Variables & Expressions** | [`def`](#def) | [`assert`](#assert) | [`print`](#print) | [Multi-line](#multi-line-expressions) **Data Types** | [JSON](#json) | [XML](#xml) | [JS Functions](#javascript-functions) | [Reading Files](#reading-files) **Primary HTTP Keywords** | [`url`](#url) | [`path`](#path) | [`request`](#request) | [`method`](#method) - | [`status`](#status) | [`multipart post`](#multipart-post) | [`soap action`](#soap-action) + | [`status`](#status) | [`multipart post`](#multipart-post) | [`soap action`](#soap-action) | [`ssl enabled`](#ssl-enabled) **Secondary HTTP Keywords** | [`param`](#param) | [`header`](#header) | [`cookie`](#cookie) | [`form field`](#form-field) | [`multipart field`](#multipart-field) | [`multipart entity`](#multipart-entity) **Set, Match, Assert** | [`set`](#set) | [`match`](#match) | [`contains`](#match-contains) | [Ignore / Vallidate](#ignore-or-validate) @@ -52,7 +52,7 @@ And you don't need to create Java objects (or POJO-s) for any of the payloads th | [`responseHeaders`](#responseheaders) | [`responseStatus`](#responsestatus) | [`responseTime`](#responsetime) **Reusable Functions** | [`call`](#call) | [`karate` object](#the-karate-object) **Tips and Tricks** | [Embedded Expressions](#embedded-expressions) | [GraphQL RegEx Example](#graphql--regex-replacement-example) | [Multi-line Comments](#multi-line-comments) | [Cucumber Tags](#cucumber-tags) - | [Data Driven Tests](#data-driven-tests) | [Auth and Headers](#sign-in-example) | [Dynamic Port Numbers](#dynamic-port-numbers) + | [Data Driven Tests](#data-driven-tests) | [Auth](#sign-in-example) and [Headers](#http-basic-authentication-example) | [Dynamic Port Numbers](#dynamic-port-numbers) # Features * Scripts are plain-text files and require no compilation step or IDE @@ -324,7 +324,7 @@ but the simplest should be to have a JUnit class (with the Karate annotation) at of your test packages (`src/test/java`, no package name). With that in position, you can do this: ``` -mvn test -Dcucumber.options="--tags ~@ignore" -Dtest=TestAll +mvn test -Dcucumber.options="--plugin junit:target/cucumber-junit.xml --tags ~@ignore" -Dtest=TestAll ``` Here, `TestAll` is the name of the Java class you designated to run all your tests. And yes, Cucumber has a neat way to [tag your tests](#cucumber-tags) and the above example demonstrates how to @@ -514,11 +514,11 @@ Karate's approach is that all the step-definitions you need in order to work wit have been already implemented. And since you can easily extend Karate [using JavaScript](#call), there is no need to compile Java code any more. -Aspect | Cucumber | Karate --------| -------- | ------ -**Step Definitions Needed** | Yes. You need to keep implementing them as your functionality grows. [This can get very tedious](https://angiejones.tech/rest-assured-with-cucumber-using-bdd-for-web-services-automation/) | No. They are ready-made. -**Layers of Code to Maintain** | 2: Cucumber (Gherkin) + Java step-definitions | 1: Cucumber (Karate-DSL) -**Natural Language** | Yes. Cucumber will read like natural language if you implement the step-definitions right. | No. Although Karate is a [true DSL](https://ayende.com/blog/2984/dsl-vs-fluent-interface-compare-contrast) it is ultimately a programming language, albeit a very simple one. But for testing web-services at the level of HTTP requests and responses, it is extremely effective. Keep in mind that web-services are not 'human-facing' by design. + - | Cucumber | Karate +------ | -------- | ------ +**Must Add Step Definitions** | Yes. You need to keep implementing them as your functionality grows. [This can get very tedious](https://angiejones.tech/rest-assured-with-cucumber-using-bdd-for-web-services-automation#comment-40). | No. +**Layers of Code to Maintain** | **2** : Gherkin (custom grammar), and corresponding Java step-definitions | **1** : Karate DSL +**Natural Language** | Yes. Cucumber will read like natural language if you implement the step-definitions right. | No. Although Karate is a [true DSL](https://ayende.com/blog/2984/dsl-vs-fluent-interface-compare-contrast), it is ultimately a mini-programming language, although a very simple one. But for testing web-services at the level of HTTP requests and responses, it is ideal. Keep in mind that web-services are not 'human-facing' by design. **BDD Syntax** | Yes | Yes One nice thing about the design of the underlying Cucumber framework is that @@ -541,9 +541,9 @@ techniques such as expressing data-tables in test scripts. With the formalities out of the way, let's dive straight into the syntax. -# Variables +# Setting and Using Variables ## `def` -### For Setting Variables +### Set a named variable ```cucumber # assigning a string value: Given def myVar = 'world' @@ -559,7 +559,7 @@ Keep in mind that the start-up [configuration routine](#configuration) could hav initialized some variables before the script even started. ## `assert` -### Assert if an Expression evaluates to `true` +### Assert if an expression evaluates to `true` Once defined, you can refer to a variable by name. Expressions are evaluated using the embedded JavaScript engine. The assert keyword can be used to assert that an expression returns a boolean value. @@ -575,7 +575,7 @@ Instead you would typically use the [`match`](#match) keyword, that is designed powerful assertions against JSON and XML response payloads. ## `print` -### Ideal for logging a message to the console +### Log to the console You can use `print` to log variables to the console in the middle of a script. All of the text to the right of the `print` keyword will be evaluated as a single expression (somewhat like [`assert`](#assert)). @@ -583,12 +583,12 @@ All of the text to the right of the `print` keyword will be evaluated as a singl * print 'the value of a is ' + a ``` -## 'Native' data types +# 'Native' data types Native data types mean that you can insert them into a script without having to worry about enclosing them in strings and then having to 'escape' double-quotes all over the place. They seamlessly fit 'in-line' within your test script. -### JSON +## JSON Note that the parser is 'lenient' so that you don't have to enclose all keys in double-quotes. ```cucumber * def cat = { name: 'Billie', scores: [2, 5] } @@ -597,7 +597,7 @@ Note that the parser is 'lenient' so that you don't have to enclose all keys in When inspecting JSON (or XML) for expected values you are probably better off using [`match`](#match) instead of `assert`. -### XML +## XML ```cucumber Given def cat = Billie25 # sadly, xpath list indexes start from 1 @@ -606,7 +606,7 @@ Then match cat/cat/scores/score[2] == '5' Then match cat.cat.scores.score[1] == 5 ``` -#### Embedded Expressions +### Embedded Expressions In the '[Hello Real World](#hello-real-world)' example, you may have noticed a short-cut hidden in the value of the 'userId' field: ```cucumber @@ -616,15 +616,15 @@ So the rule is - if a string value within a JSON (or XML) object declaration is between `#(` and `)` - it will be evaluated as a JavaScript expression. And any variables which are alive in the context can be used in this expression. -This comes in useful in some cases - and side-steps having to use JavaScript functions or -JSON-Path expressions to manipulate JSON. So you get the best of both worlds: +This comes in useful in some cases - and avoids needing to use JavaScript functions or +JSON-Path expressions to [manipulate JSON](#set). So you get the best of both worlds: the elegance of JSON to express complex nested data - while at the same time being able to dynamically plug values (that could be also JSON trees) into a JSON 'template'. -The [GraphQL / RegEx Replacement example](#graphql--regex-replacement-example) also includes usage -of 'embedded expressions'. +The [GraphQL / RegEx Replacement example](#graphql--regex-replacement-example) also demonstrates the usage +of 'embedded expressions', e.g. `'#(query)'`. -#### Multi-Line Expressions +### Multi-Line Expressions The keywords [`def`](#def), [`set`](#set), [`match`](#match) and [`request`](#request) take multi-line input as the last argument. This is useful when you want to express a one-off lengthy snippet of text in-line, without having to split it out into a separate [file](#reading-files). Here are some examples: @@ -701,6 +701,7 @@ If you want to do advanced stuff such as make HTTP requests within a function - that is what the [`call`](#call) keyword is for. [More examples](#calling-java) of calling Java appear later in this document. + ## Reading Files This actually is a good example of how you could extend Karate with custom functions. `read()` is a JavaScript function that is automatically available when Karate starts. @@ -737,12 +738,12 @@ which is typically what you would need for [`multipart`](#multipart-field) file * def someStream = read('some-pdf.pdf') ``` -## Core Keywords +# Core Keywords They are `url`, `path`, `request`, `method` and `status`. These are essential HTTP operations, they focus on setting one (non-keyed) value at a time and don't involve any '=' signs in the syntax. -### `url` +## `url` ```cucumber Given url 'https://myhost.com/v1/cats' ``` @@ -754,7 +755,7 @@ can come from global [config](#configuration). ```cucumber Given url 'https://' + e2eHostName + '/v1/api' ``` -### `path` +## `path` REST-style path parameters. Can be expressions that will be evaluated. Comma delimited values are supported which can be more convenient, and takes care of URL-encoding and appending '/' where needed. ```cucumber @@ -768,25 +769,33 @@ Given path 'documents' And path documentId And path 'download' ``` -### `request` +## `request` In-line JSON: ```cucumber -When request { name: 'Billie', type: 'LOL' } +Given request { name: 'Billie', type: 'LOL' } ``` In-line XML: ```cucumber -When request BillieCeiling +And request BillieCeiling ``` From a [file](#reading-files) in the same package. Use the `classpath:` prefix to load from the classpath instead. ```cucumber -When request read('my-json.json') +Given request read('my-json.json') ``` You could always use a variable: ```cucumber -When request myVariable +And request myVariable +``` +Defining the `request` is mandatory if you are using an HTTP `method` that expects a body such as +`post`. You can always specify an empty body as follows, and force the right `Content-Type` header +by using the [`header`](#header) keyword. +```cucumber +Given request '' +And header Content-Type = 'text/html' ``` -### `method` + +## `method` The HTTP verb - `get`, `post`, `put`, `delete`, `patch`, `options`, `head`, `connect`, `trace`. Lower-case is fine. @@ -803,7 +812,7 @@ When method get # the step that immediately follows the above would typically be: Then status 200 ``` -### `status` +## `status` This is a shortcut to assert the HTTP response code. ```cucumber Then status 200 @@ -812,18 +821,18 @@ And this assertion will cause the test to fail if the HTTP response code is some See also [`responseStatus`](#responsestatus). -## Keywords that set key-value pairs +# Keywords that set key-value pairs They are `param`, `header`, `cookie`, `form field` and `multipart field`. The syntax will include a '=' sign between the key and the value. The key does not need to be within quotes. -### `param` +## `param` Setting query-string parameters: ```cucumber Given param someKey = 'hello' And param anotherKey = someVariable ``` -### `header` +## `header` You can even use functions or expressions: ```cucumber Given header Authorization = myAuthFunction() @@ -839,17 +848,17 @@ And header Accept = 'application/json' When method post Then status 200 ``` -### `cookie` +## `cookie` Setting a cookie: ```cucumber Given cookie foo = 'bar' ``` -### `form field` +## `form field` These would be URL-encoded when the HTTP request is submitted (by the [`method`](#method) step). ```cucumber Given form field foo = 'bar' ``` -### `multipart field` +## `multipart field` Use this for building multipart named (form) field requests. The submit has to be issued with [`multipart post`](#multipart-post). @@ -858,7 +867,7 @@ Given multipart field file = read('test.pdf') And multipart field fileName = 'custom-name.pdf' ``` -### `multipart entity` +## `multipart entity` > This is technically not in the key-value form: `multipart field name = 'foo'`, but logically belongs here in the documentation. @@ -875,8 +884,8 @@ When multipart post Then status 201 ``` -## A couple more commands -### `multipart post` +# Multipart and SOAP +## `multipart post` Since a multipart request needs special handling, this is a rare case where the [`method`](#method) step is not used to actually fire the request to the server. The only other exception is `soap action` (see below). @@ -888,7 +897,7 @@ The `multipart post` step will default to setting the `Content-Type` header as ` You can over-ride it by using the [`header`](#header) keyword before this step. Look at [`multipart entity`](#multipart-entity) for an example. -### `soap action` +## `soap action` The name of the SOAP action specified is used as the 'SOAPAction' header. Here is an example which also demonstrates how you could assert for expected values in the response XML. ```cucumber @@ -899,6 +908,13 @@ And match response /Envelope/Body/QueryUsageBalanceResponse/Result/Error/Code == And match response /Envelope/Body/QueryUsageBalanceResponse == read('expected-response.xml') ``` +## `ssl enabled` +This switches on Karate's support for making HTTPS calls without needing to configure a trusted certificate +or key-store. It is recommended that you do this at the start of your script or in the `Background:` section. +```cucumber +* ssl enabled +``` + # Preparing, Manipulating and Matching Data One of the most time-consuming parts of writing tests for web-services is traversing the response payload and checking for expected results and data. You can appreciate how @@ -989,7 +1005,7 @@ XML and XPath is similar. * match cat / == Jean ``` -### Ignore or Validate +## Ignore or Validate When expressing expected results (in JSON or XML) you can mark some fields to be ignored when the match (comparison) is performed. You can even use a regular-expression so that instead of checking for equality, Karate will just validate that the actual value conforms to the expected @@ -1021,7 +1037,7 @@ Marker | Description #regexSTR | Expects actual (string) value to match the regular-expression 'STR' #?EXPR | Expects the JavaScript expression 'EXPR' to evaluate to true (see examples below) -#### 'Self' Validation Expressions +### 'Self' Validation Expressions The special 'predicate' marker in the last row of the table above is an interesting one. It is best explained via examples. @@ -1044,7 +1060,7 @@ And functions work as well ! You can imagine how you could evolve a nice set of validate all your domain objects. ```cucumber * def date = { month: 3 } -* def isValidMonth = function(v) { return v >= 0 && v <= 12 } +* def isValidMonth = function(m) { return m >= 0 && m <= 12 } * match date == { month: '#? isValidMonth(_)' } ``` @@ -1064,7 +1080,7 @@ Then match response == read('test.pdf') ``` Checking if a string is contained within another string is a very common need and -[`match [name] contains`](#match-contains) works just like you'd expect: +[`match` (name) `contains`](#match-contains) works just like you'd expect: ```cucumber * def hello = 'Hello World!' * match hello contains 'World' @@ -1078,6 +1094,8 @@ reduces some complexity - because strictly, HTTP headers are a 'multi-valued map ```cucumber # so after a http request Then match header Content-Type == 'application/json' +# 'contains' works as well +Then match header Content-Type contains 'application' ``` Note the extra convenience where you don't have to enclose the LHS key in quotes. @@ -1088,9 +1106,10 @@ if you wanted to do more checks, but you typically won't need to. ### `match contains` #### JSON Keys In some cases where the response JSON is wildly dynamic, you may want to only check for the existence of -some keys. And `match [name] contains` is how you can do so: +some keys. And `match` (name) `contains` is how you can do so: ```cucumber * def foo = { bar: 1, baz: 'hello', ban: 'world' } + * match foo contains { bar: 1 } * match foo contains { baz: 'hello' } * match foo contains { bar:1, baz: 'hello' } @@ -1100,12 +1119,11 @@ some keys. And `match [name] contains` is how you can do so: #### JSON Arrays -This is a good time to discuss JsonPath, which is perfect for slicing and dicing JSON into manageable chunks, -against which you can run assertions. It is worth looking at the reference and examples here: -[JsonPath Examples](https://github.com/jayway/JsonPath#path-examples). +This is a good time to discuss JsonPath, which is perfect for slicing and dicing JSON into manageable chunks. +It is worth taking time to go through the documentation and examples here: [JsonPath Examples](https://github.com/jayway/JsonPath#path-examples). -As just one example, let us perform some assertions after scraping a list of child elements out of the JSON -below. +Here are some example assertions performed while scraping a list of child elements out of the JSON below. +Observe how you can `match` the result of a JsonPath expression with your expected data. ```cucumber Given def cat = @@ -1138,13 +1156,32 @@ Then match cat.rivals[*] contains [{ id: 42, name: '#ignore' }] It is worth mentioning that to do the equivalent of the last line in Java, you would typically have to traverse 2 Java Objects, one of which is within a list, and you would have to check for nulls as well. -When you use Karate, all your data assertions can be done in pure JSON and without needing a whole -forest of companion Java objects. And when you read your JSON objects from (re-usable) files, -your response payload assertions can be accomplished in just one line. +When you use Karate, all your data assertions can be done in pure JSON and without needing a thick +forest of companion Java objects. And when you [`read`](#read) your JSON objects from (re-usable) files, +even complex response payload assertions can be accomplished in just a single line of Karate-script. -## Special Variables +## Matching All Array Elements +### `match each` +Karate has syntax sugar that can iterate over all elements in a JSON array. Here's how it works: +```cucumber +* def data = { foo: [{ bar: 1, baz: 'a' }, { bar: 2, baz: 'b' }, { bar: 3, baz: 'c' }]} + +* match each data.foo == { bar: '#number', baz: '#string' } + +# and you can use 'contains' the way you'd expect +* match each data.foo contains { bar: '#number' } +* match each data.foo contains { bar: '#? _ != 4' } + +# some more examples of validation macros +* match each data.foo contains { baz: "#? _ != 'z'" } +* def isAbc = function(x) { return x == 'a' || x == 'b' || x == 'c' } +* match each data.foo contains { baz: '#? isAbc(_)' } -### `response` +``` + +# Special Variables + +## `response` After every HTTP call this variable is set with the response and is available until the next HTTP request over-writes it. @@ -1179,7 +1216,7 @@ Then match response/cat/name == 'Billie' Then match /cat/name == 'Billie' ``` -### `cookies` +## `cookies` The `cookies` variable is set upon any HTTP response and is a map-like (or JSON-like) object. It can be easily inspected or used in expressions. ```cucumber @@ -1192,7 +1229,7 @@ browser - which makes it easy to script things like HTML-form based authenticati Of course you can manipulate `cookies` or even set it to `null` if you wish - at any point within a test script. -### `responseHeaders` +## `responseHeaders` See also [`match header`](#match-header) which is what you would normally need. But if you need to use values in the response headers - they will be in a variable @@ -1201,13 +1238,13 @@ like this: ```cucumber * def contentType = responseHeaders['Content-Type'][0] ``` -### `responseStatus` +## `responseStatus` You would normally only need to use the [`status`](#status) keyword. But if you really need to use the HTTP response code in an expression or save it for later, you can get it as an integer: ```cucumber * def uploadStatusCode = responseStatus ``` -### `responseTime` +## `responseTime` The response time (in milliseconds) for every HTTP request would be available in a variable called `responseTime`. You can use this to assert that the response was returned within the expected time like so: @@ -1216,13 +1253,13 @@ When method post Then status 201 And assert responseTime < 1000 ``` -### `read` +## `read` This is a great example of how you can extend Karate by defining your own functions. Behind the scenes the `read(filename)` function is actually implemented in JavaScript. Refer to the section on [reading files](#reading-files) for how to use this built-in function. -### `headers` +## `headers` This is a convenience feature to make custom header manipulation as easy as possible. For every HTTP request made from Karate, the internal flow is as follows: * does a variable called `headers` exist? @@ -1281,9 +1318,8 @@ With this convenience comes the caveat - you could over-write the [`headers`](#h But there are cases where you may want to do this intentionally - for example when a few steps in your flow need to change (or completely bypass) the currently-set header-manipulation scheme. -# `call` -## Advanced JavaScript Function Invocation - +# Advanced JavaScript Function Invocation +## `call` This is one of the most powerful features of Karate. With `call` you can: * call re-usable functions that take complex data as an argument and return complex data that can be stored in a variable * share and re-use functionality across your organization @@ -1292,7 +1328,7 @@ This is one of the most powerful features of Karate. With `call` you can: A very common use-case is the inevitable 'sign-in' that you need to perform at the beginning of every test script. -## Sign-In Example +### Sign-In Example This example actually makes two HTTP requests - the first is a standard 'sign-in' POST and then (for illustrative purposes) another GET is made for retrieving a list of projects for the @@ -1508,10 +1544,10 @@ In situations where you start an (embedded) application server as part of the te challenge is that the HTTP port may be determined at run-time. So how can you get this value injected into the Karate configuration ? -It so happens that the [`karate`](#the-karate-object) object has an accessor called `properties` (of type array) -which can read a Java system-property. Since the `karate` object is available for you to use -within [`karate-config.js`](#configuration), it is a simple and effective way for other processes within -the same JVM to pass configuration values into Karate at run-time. +It so happens that the [`karate`](#the-karate-object) object has a field called `properties` +which can read a Java system-property by name: `properties[key]`. Since the `karate` object is injected +within [`karate-config.js`](#configuration) on start-up, it is a simple and effective way for other +processes within the same JVM to pass configuration values into Karate at run-time. You can look at the [Wiremock](http://wiremock.org) based unit-test code of Karate to see how this can be done. * [HelloWorldTest.java](karate-core/src/test/java/com/intuit/karate/wiremock/HelloWorldTest.java) - see line #30 diff --git a/karate-core/src/main/java/com/intuit/karate/MatchType.java b/karate-core/src/main/java/com/intuit/karate/MatchType.java new file mode 100644 index 000000000..97f0f58fa --- /dev/null +++ b/karate-core/src/main/java/com/intuit/karate/MatchType.java @@ -0,0 +1,14 @@ +package com.intuit.karate; + +/** + * + * @author pthomas3 + */ +public enum MatchType { + + EQUALS, + CONTAINS, + EACH_EQUALS, + EACH_CONTAINS + +} diff --git a/karate-core/src/main/java/com/intuit/karate/Script.java b/karate-core/src/main/java/com/intuit/karate/Script.java index 74eeb1fd4..8dc1f0ecf 100755 --- a/karate-core/src/main/java/com/intuit/karate/Script.java +++ b/karate-core/src/main/java/com/intuit/karate/Script.java @@ -340,10 +340,10 @@ public static boolean isQuoted(String exp) { } public static AssertionResult matchNamed(String name, String path, String expected, ScriptContext context) { - return matchNamed(false, name, path, expected, context); + return matchNamed(MatchType.EQUALS, name, path, expected, context); } - public static AssertionResult matchNamed(boolean contains, String name, String path, String expected, ScriptContext context) { + public static AssertionResult matchNamed(MatchType matchType, String name, String path, String expected, ScriptContext context) { name = StringUtils.trim(name); if (isJsonPath(name) || isXmlPath(name)) { // short-cut for operating on response path = name; @@ -357,39 +357,39 @@ public static AssertionResult matchNamed(boolean contains, String name, String p } expected = StringUtils.trim(expected); if ("header".equals(name)) { // convenience shortcut for asserting against response header - return matchNamed(contains, ScriptValueMap.VAR_RESPONSE_HEADERS, "$['" + path + "'][0]", expected, context); + return matchNamed(matchType, ScriptValueMap.VAR_RESPONSE_HEADERS, "$['" + path + "'][0]", expected, context); } else { ScriptValue actual = context.vars.get(name); - switch(actual.getType()) { + switch (actual.getType()) { case STRING: case INPUT_STREAM: - return matchString(contains, actual, expected, path, context); + return matchString(matchType, actual, expected, path, context); case XML: if ("$".equals(path)) { path = "/"; // edge case where the name was 'response' } if (!isJsonPath(path)) { - return matchXmlPath(contains, actual, path, expected, context); + return matchXmlPath(matchType, actual, path, expected, context); } - // break; - // fall through to JSON. yes, dot notation can be used on XML + // break; + // fall through to JSON. yes, dot notation can be used on XML !! default: - return matchJsonPath(contains, actual, path, expected, context); + return matchJsonPath(matchType, actual, path, expected, context); } } } - public static AssertionResult matchString(boolean contains, ScriptValue actual, String expected, String path, ScriptContext context) { + public static AssertionResult matchString(MatchType matchType, ScriptValue actual, String expected, String path, ScriptContext context) { ScriptValue expectedValue = preEval(expected, context); expected = expectedValue.getAsString(); - return matchStringOrPattern(contains, actual, expected, path, context); + return matchStringOrPattern(matchType, actual, expected, path, context); } public static boolean isValidator(String text) { return text.startsWith("#"); } - public static AssertionResult matchStringOrPattern(boolean contains, ScriptValue actValue, String expected, String path, ScriptContext context) { + public static AssertionResult matchStringOrPattern(MatchType matchType, ScriptValue actValue, String expected, String path, ScriptContext context) { if (expected == null) { if (!actValue.isNull()) { return matchFailed(path, actValue.getValue(), expected); @@ -422,7 +422,7 @@ public static AssertionResult matchStringOrPattern(boolean contains, ScriptValue } } else { String actual = actValue.getAsString(); - if (contains) { + if (matchType == MatchType.CONTAINS) { if (!actual.contains(expected)) { return matchFailed(path, actual, expected + " (not a sub-string)"); } @@ -433,11 +433,11 @@ public static AssertionResult matchStringOrPattern(boolean contains, ScriptValue return AssertionResult.PASS; } - public static AssertionResult matchXmlObject(boolean contains, Object act, Object exp, ScriptContext context) { - return matchNestedObject('/', "", contains, act, exp, context); + public static AssertionResult matchXmlObject(MatchType matchType, Object act, Object exp, ScriptContext context) { + return matchNestedObject('/', "", matchType, act, exp, context); } - public static AssertionResult matchXmlPath(boolean contains, ScriptValue actual, String path, String expression, ScriptContext context) { + public static AssertionResult matchXmlPath(MatchType matchType, ScriptValue actual, String path, String expression, ScriptContext context) { Document actualDoc = actual.getValue(Document.class); Node actNode = XmlUtils.getNodeByPath(actualDoc, path); ScriptValue expected = preEval(expression, context); @@ -453,10 +453,10 @@ public static AssertionResult matchXmlPath(boolean contains, ScriptValue actual, actObject = new ScriptValue(actNode).getAsString(); expObject = expected.getAsString(); } - return matchNestedObject('/', path, contains, actObject, expObject, context); + return matchNestedObject('/', path, matchType, actObject, expObject, context); } - public static AssertionResult matchJsonPath(boolean contains, ScriptValue actual, String path, String expression, ScriptContext context) { + public static AssertionResult matchJsonPath(MatchType matchType, ScriptValue actual, String path, String expression, ScriptContext context) { DocumentContext actualDoc; switch (actual.getType()) { case JSON: @@ -476,7 +476,7 @@ public static AssertionResult matchJsonPath(boolean contains, ScriptValue actual if (!expected.isString()) { return matchFailed(path, actualString, expected.getValue()); } else { - return matchStringOrPattern(contains, actual, expected.getValue(String.class), path, context); + return matchStringOrPattern(matchType, actual, expected.getValue(String.class), path, context); } default: throw new RuntimeException("not json, cannot do json path for value: " + actual + ", path: " + path); @@ -491,15 +491,40 @@ public static AssertionResult matchJsonPath(boolean contains, ScriptValue actual default: expObject = expected.getValue(); } - return matchNestedObject('.', path, contains, actObject, expObject, context); + switch (matchType) { + case CONTAINS: + case EQUALS: + return matchNestedObject('.', path, matchType, actObject, expObject, context); + case EACH_EQUALS: + case EACH_CONTAINS: + if (actObject instanceof List) { + List actList = (List) actObject; + MatchType listMatchType = matchType == MatchType.EACH_CONTAINS ? MatchType.CONTAINS : MatchType.EQUALS; + int actSize = actList.size(); + for (int i = 0; i < actSize; i++) { + Object actListObject = actList.get(i); + String listPath = path + "[" + i + "]"; + AssertionResult ar = matchNestedObject('.', listPath, listMatchType, actListObject, expObject, context); + if (!ar.pass) { + return ar; + } + } + return AssertionResult.PASS; + } else { + throw new RuntimeException("'match all' failed, not a json array: + " + actual + ", path: " + path); + } + default: + // dead code + return AssertionResult.PASS; + } } public static AssertionResult matchJsonObject(Object act, Object exp, ScriptContext context) { - return matchNestedObject('.', "$", false, act, exp, context); + return matchNestedObject('.', "$", MatchType.EQUALS, act, exp, context); } - public static AssertionResult matchJsonObject(boolean contains, Object act, Object exp, ScriptContext context) { - return matchNestedObject('.', "$", contains, act, exp, context); + public static AssertionResult matchJsonObject(MatchType matchType, Object act, Object exp, ScriptContext context) { + return matchNestedObject('.', "$", matchType, act, exp, context); } public static AssertionResult matchFailed(String path, Object actObject, Object expObject) { @@ -508,7 +533,7 @@ public static AssertionResult matchFailed(String path, Object actObject, Object return AssertionResult.fail(message); } - public static AssertionResult matchNestedObject(char delimiter, String path, boolean contains, Object actObject, Object expObject, ScriptContext context) { + public static AssertionResult matchNestedObject(char delimiter, String path, MatchType matchType, Object actObject, Object expObject, ScriptContext context) { logger.trace("path: {}, actual: '{}', expected: '{}'", path, actObject, expObject); if (expObject == null) { if (actObject != null) { @@ -518,20 +543,20 @@ public static AssertionResult matchNestedObject(char delimiter, String path, boo } if (expObject instanceof String) { ScriptValue actValue = new ScriptValue(actObject); - return matchStringOrPattern(contains, actValue, expObject.toString(), path, context); + return matchStringOrPattern(matchType, actValue, expObject.toString(), path, context); } else if (expObject instanceof Map) { if (!(actObject instanceof Map)) { return matchFailed(path, actObject, expObject); } Map expMap = (Map) expObject; Map actMap = (Map) actObject; - if (!contains && actMap.size() > expMap.size()) { // > is because of the chance of #ignore + if (matchType != MatchType.CONTAINS && actMap.size() > expMap.size()) { // > is because of the chance of #ignore return matchFailed(path, actObject, expObject); } for (Map.Entry expEntry : expMap.entrySet()) { String key = expEntry.getKey(); String childPath = path + delimiter + key; - AssertionResult ar = matchNestedObject(delimiter, childPath, Boolean.FALSE, actMap.get(key), expEntry.getValue(), context); + AssertionResult ar = matchNestedObject(delimiter, childPath, MatchType.EQUALS, actMap.get(key), expEntry.getValue(), context); if (!ar.pass) { return ar; } @@ -542,16 +567,16 @@ public static AssertionResult matchNestedObject(char delimiter, String path, boo List actList = (List) actObject; int actCount = actList.size(); int expCount = expList.size(); - if (!contains && actCount != expCount) { + if (matchType != MatchType.CONTAINS && actCount != expCount) { return matchFailed(path, actObject, expObject); } - if (contains) { // just checks for existence + if (matchType == MatchType.CONTAINS) { // just checks for existence for (Object expListObject : expList) { // for each expected item in the list boolean found = false; for (int i = 0; i < actCount; i++) { Object actListObject = actList.get(i); String listPath = path + "[" + i + "]"; - AssertionResult ar = matchNestedObject(delimiter, listPath, Boolean.FALSE, actListObject, expListObject, context); + AssertionResult ar = matchNestedObject(delimiter, listPath, MatchType.EQUALS, actListObject, expListObject, context); if (ar.pass) { // exact match, we found it found = true; break; @@ -567,7 +592,7 @@ public static AssertionResult matchNestedObject(char delimiter, String path, boo Object expListObject = expList.get(i); Object actListObject = actList.get(i); String listPath = path + "[" + i + "]"; - AssertionResult ar = matchNestedObject(delimiter, listPath, Boolean.FALSE, actListObject, expListObject, context); + AssertionResult ar = matchNestedObject(delimiter, listPath, MatchType.EQUALS, actListObject, expListObject, context); if (!ar.pass) { return matchFailed(path, actObject, expObject); } diff --git a/karate-core/src/main/java/com/intuit/karate/ScriptContext.java b/karate-core/src/main/java/com/intuit/karate/ScriptContext.java index c92617ad1..68e5d6292 100755 --- a/karate-core/src/main/java/com/intuit/karate/ScriptContext.java +++ b/karate-core/src/main/java/com/intuit/karate/ScriptContext.java @@ -3,6 +3,7 @@ import com.intuit.karate.validator.Validator; import java.util.Map; import java.util.logging.Level; +import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; @@ -17,22 +18,22 @@ * @author pthomas3 */ public class ScriptContext { - + private static final Logger logger = LoggerFactory.getLogger(ScriptContext.class); - + private static final String KARATE_NAME = "karate"; - + protected final ScriptValueMap vars; - protected final Client client; + protected Client client; protected final Map validators; protected final String featureDir; protected final ClassLoader fileClassLoader; - protected final String env; + protected final String env; // needed for 3rd party code public ScriptValueMap getVars() { return vars; - } + } public ScriptContext(boolean test, String featureDir, ClassLoader fileClassLoader, String env) { this.featureDir = featureDir; @@ -47,19 +48,6 @@ public ScriptContext(boolean test, String featureDir, ClassLoader fileClassLoade client = null; return; } - ClientBuilder clientBuilder = ClientBuilder.newBuilder() - .register(MultiPartFeature.class); - if (logger.isDebugEnabled()) { - clientBuilder.register(new LoggingFeature( - java.util.logging.Logger.getLogger(LoggingFeature.DEFAULT_LOGGER_NAME), - Level.SEVERE, - LoggingFeature.Verbosity.PAYLOAD_TEXT, null)); - } - SSLContext sslContext = SslUtils.getSslContext(); - clientBuilder.sslContext(sslContext); - clientBuilder.hostnameVerifier((host, session) -> true); - clientBuilder.register(new RequestFilter(this)); - client = clientBuilder.build(); // auto config try { Script.callAndUpdateVars("read('classpath:karate-config.js')", null, this); @@ -67,8 +55,27 @@ public ScriptContext(boolean test, String featureDir, ClassLoader fileClassLoade logger.warn("start-up configuration failed, missing or bad 'karate-config.js' - {}", e.getMessage()); } logger.trace("karate context init - initial properties: {}", vars); + buildClient(null); } - + + public void buildClient(SSLContext ssl) { + ClientBuilder clientBuilder = ClientBuilder.newBuilder().register(MultiPartFeature.class); + if (logger.isDebugEnabled()) { + clientBuilder.register(new LoggingFeature( + java.util.logging.Logger.getLogger(LoggingFeature.DEFAULT_LOGGER_NAME), + Level.SEVERE, + LoggingFeature.Verbosity.PAYLOAD_TEXT, null)); + } + clientBuilder.register(new RequestFilter(this)); + if (ssl != null) { + logger.info("ssl enabled, initializing generic trusted certificate / key-store"); + HttpsURLConnection.setDefaultSSLSocketFactory(ssl.getSocketFactory()); + clientBuilder.sslContext(ssl); + clientBuilder.hostnameVerifier((host, session) -> true); + } + client = clientBuilder.build(); + } + public void injectInto(ScriptObjectMirror som) { som.setMember(KARATE_NAME, new ScriptBridge(this)); // convenience for users, can use 'karate' instead of 'this.karate' @@ -76,7 +83,7 @@ public void injectInto(ScriptObjectMirror som) { Map simple = Script.simplify(vars); for (Map.Entry entry : simple.entrySet()) { som.put(entry.getKey(), entry.getValue()); // update eval context - } + } } - + } diff --git a/karate-core/src/main/java/com/intuit/karate/SslUtils.java b/karate-core/src/main/java/com/intuit/karate/SslUtils.java index 4277296cd..81b174096 100644 --- a/karate-core/src/main/java/com/intuit/karate/SslUtils.java +++ b/karate-core/src/main/java/com/intuit/karate/SslUtils.java @@ -44,8 +44,7 @@ public void checkClientTrusted(X509Certificate[] chain, String authType) throws ctx.init(null, certs, new SecureRandom()); } catch (Exception e) { throw new RuntimeException(e); - } - HttpsURLConnection.setDefaultSSLSocketFactory(ctx.getSocketFactory()); + } return ctx; } diff --git a/karate-core/src/main/java/com/intuit/karate/StepDefs.java b/karate-core/src/main/java/com/intuit/karate/StepDefs.java index 18c916b68..9d397675a 100755 --- a/karate-core/src/main/java/com/intuit/karate/StepDefs.java +++ b/karate-core/src/main/java/com/intuit/karate/StepDefs.java @@ -10,6 +10,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.net.ssl.SSLContext; import javax.ws.rs.client.Entity; import javax.ws.rs.client.Invocation; import javax.ws.rs.client.WebTarget; @@ -52,6 +53,12 @@ public StepDefs(String featureDir, ClassLoader fileClassLoader, String env) { public ScriptContext getContext() { return context; } + + @When("^ssl enabled") + public void sslEnabled() { + SSLContext ssl = SslUtils.getSslContext(); + context.buildClient(ssl); + } @When("^url (.+)") public void url(String expression) { @@ -312,7 +319,7 @@ public void soapAction(String action) { } catch (Exception e) { logger.warn("xml parsing failed, response data type set to string: {}", e.getMessage()); context.vars.put(ScriptValueMap.VAR_RESPONSE, rawResponse); - } + } } private MultiPart getMultiPart() { @@ -389,28 +396,37 @@ public void status(int status) { assertEquals(status, response.getStatus()); } - @Then("^match ([^\\s]+)( .+)? ==$") - public void matchVariableDocString(String name, String path, String expected) { - matchNamed(false, name, path, expected); + private static MatchType toMatchType(String each, boolean contains) { + return each == null + ? contains ? MatchType.CONTAINS : MatchType.EQUALS + : contains ? MatchType.EACH_CONTAINS : MatchType.EACH_EQUALS; + } + + @Then("^match (each )?([^\\s]+)( .+)? ==$") + public void matchEqualsDocString(String each, String name, String path, String expected) { + matchEquals(each, name, path, expected); } - @Then("^match ([^\\s]+)( .+)? contains$") - public void matchContainsDocString(String name, String path, String expected) { - matchNamed(true, name, path, expected); + @Then("^match (each )?([^\\s]+)( .+)? contains$") + public void matchContainsDocString(String each, String name, String path, String expected) { + matchContains(each, name, path, expected); } - @Then("^match ([^\\s]+)( .+)? == (.+)") - public void matchVariable(String name, String path, String expected) { - matchNamed(false, name, path, expected); + + @Then("^match (each )?([^\\s]+)( .+)? == (.+)") + public void matchEquals(String each, String name, String path, String expected) { + MatchType mt = toMatchType(each, false); + matchNamed(mt, name, path, expected); } - @Then("^match ([^\\s]+)( .+)? contains (.+)") - public void matchContains(String name, String path, String expected) { - matchNamed(true, name, path, expected); + @Then("^match (each )?([^\\s]+)( .+)? contains (.+)") + public void matchContains(String each, String name, String path, String expected) { + MatchType mt = toMatchType(each, true); + matchNamed(mt, name, path, expected); } - public void matchNamed(boolean contains, String name, String path, String expected) { - AssertionResult ar = Script.matchNamed(contains, name, path, expected, context); + public void matchNamed(MatchType matchType, String name, String path, String expected) { + AssertionResult ar = Script.matchNamed(matchType, name, path, expected, context); handleFailure(ar); } diff --git a/karate-core/src/test/java/com/intuit/karate/ScriptTest.java b/karate-core/src/test/java/com/intuit/karate/ScriptTest.java index d40f9a432..94791358c 100755 --- a/karate-core/src/test/java/com/intuit/karate/ScriptTest.java +++ b/karate-core/src/test/java/com/intuit/karate/ScriptTest.java @@ -244,7 +244,7 @@ public void testMatchListObjects() { Map rightChild = new HashMap<>(); rightChild.put("a", 1); right.add(rightChild); - Script.matchJsonObject(left, right, null); + assertTrue(Script.matchJsonObject(left, right, null).pass); } @Test @@ -253,19 +253,32 @@ public void testMatchJsonPath() { ScriptContext ctx = getContext(); ctx.vars.put("myJson", doc); ScriptValue myJson = ctx.vars.get("myJson"); - Script.matchJsonPath(false, myJson, "$.foo", "'bar'", ctx); - Script.matchJsonPath(false, myJson, "$.baz", "{ ban: [1, 2, 3]} }", ctx); - Script.matchJsonPath(false, myJson, "$.baz.ban[1]", "2", ctx); - Script.matchJsonPath(false, myJson, "$.baz", "{ ban: [1, '#ignore', 3]} }", ctx); + assertTrue(Script.matchJsonPath(MatchType.EQUALS, myJson, "$.foo", "'bar'", ctx).pass); + assertTrue(Script.matchJsonPath(MatchType.EQUALS, myJson, "$.baz", "{ ban: [1, 2, 3]} }", ctx).pass); + assertTrue(Script.matchJsonPath(MatchType.EQUALS, myJson, "$.baz.ban[1]", "2", ctx).pass); + assertTrue(Script.matchJsonPath(MatchType.EQUALS, myJson, "$.baz", "{ ban: [1, '#ignore', 3]} }", ctx).pass); } + + @Test + public void testMatchAllJsonPath() { + DocumentContext doc = JsonPath.parse("{ foo: [{bar: 1, baz: 'a'}, {bar: 2, baz: 'b'}, {bar:3, baz: 'c'}]}"); + ScriptContext ctx = getContext(); + ctx.vars.put("myJson", doc); + ScriptValue myJson = ctx.vars.get("myJson"); + assertTrue(Script.matchJsonPath(MatchType.EQUALS, myJson, "$.foo", "[{bar: 1, baz: 'a'}, {bar: 2, baz: 'b'}, {bar:3, baz: 'c'}]", ctx).pass); + assertTrue(Script.matchJsonPath(MatchType.EACH_EQUALS, myJson, "$.foo", "{bar:'#number', baz:'#string'}", ctx).pass); + assertTrue(Script.matchJsonPath(MatchType.EACH_CONTAINS, myJson, "$.foo", "{bar:'#number'}", ctx).pass); + assertTrue(Script.matchJsonPath(MatchType.EACH_CONTAINS, myJson, "$.foo", "{baz:'#string'}", ctx).pass); + assertFalse(Script.matchJsonPath(MatchType.EACH_EQUALS, myJson, "$.foo", "{bar:'#? _ < 3', baz:'#string'}", ctx).pass); + } @Test public void testMatchJsonPathOnResponse() { DocumentContext doc = JsonPath.parse("{ foo: 'bar' }"); ScriptContext ctx = getContext(); ctx.vars.put("response", doc); - Script.matchNamed("$", null, "{ foo: 'bar' }", ctx); - Script.matchNamed("$.foo", null, "'bar'", ctx); + assertTrue(Script.matchNamed("$", null, "{ foo: 'bar' }", ctx).pass); + assertTrue(Script.matchNamed("$.foo", null, "'bar'", ctx).pass); } private final String ACTUAL = "{\"id\":{\"domain\":\"ACS\",\"type\":\"entityId\",\"value\":\"bef90f66-bb57-4fea-83aa-a0acc42b0426\"},\"primaryId\":\"bef90f66-bb57-4fea-83aa-a0acc42b0426\",\"created\":{\"on\":\"2016-02-28T05:56:48.485+0000\"},\"lastUpdated\":{\"on\":\"2016-02-28T05:56:49.038+0000\"},\"organization\":{\"id\":{\"domain\":\"ACS\",\"type\":\"entityId\",\"value\":\"631fafe9-8822-4c82-b4a4-8735b202c16c\"},\"created\":{\"on\":\"2016-02-28T05:56:48.486+0000\"},\"lastUpdated\":{\"on\":\"2016-02-28T05:56:49.038+0000\"}},\"clientState\":\"ACTIVE\"}"; @@ -279,7 +292,7 @@ public void testMatchTwoJsonDocsWithIgnores() { ctx.vars.put("actual", actual); ctx.vars.put("expected", expected); ScriptValue act = ctx.vars.get("actual"); - Script.matchJsonPath(false, act, "$", "expected", ctx); + assertTrue(Script.matchJsonPath(MatchType.EQUALS, act, "$", "expected", ctx).pass); } @Test @@ -288,9 +301,9 @@ public void testMatchXmlPath() { Document doc = XmlUtils.toXmlDoc("barworld"); ctx.vars.put("myXml", doc); ScriptValue myXml = ctx.vars.get("myXml"); - Script.matchXmlPath(false, myXml, "/root/foo", "'bar'", ctx); - Script.matchXmlPath(false, myXml, "/root/foo", "bar", ctx); - Script.matchXmlPath(false, myXml, "/root/hello", "'world'", ctx); + assertTrue(Script.matchXmlPath(MatchType.EQUALS, myXml, "/root/foo", "'bar'", ctx).pass); + assertTrue(Script.matchXmlPath(MatchType.EQUALS, myXml, "/root/foo", "bar", ctx).pass); + assertTrue(Script.matchXmlPath(MatchType.EQUALS, myXml, "/root/hello", "'world'", ctx).pass); } @Test @@ -298,17 +311,17 @@ public void testAssignAndMatchXml() { ScriptContext ctx = getContext(); Script.assign("myXml", "bar", ctx); Script.assign("myStr", "myXml/root/foo", ctx); - Script.assertBoolean("myStr == 'bar'", ctx); + assertTrue(Script.assertBoolean("myStr == 'bar'", ctx).pass); } @Test public void testXmlShortCutsForResponse() { ScriptContext ctx = getContext(); Script.assign("response", "bar", ctx); - Script.matchNamed("response", "/", "bar", ctx); - Script.matchNamed("response/", null, "bar", ctx); - Script.matchNamed("response", null, "bar", ctx); - Script.matchNamed("/", null, "bar", ctx); + assertTrue(Script.matchNamed("response", "/", "bar", ctx).pass); + assertTrue(Script.matchNamed("response/", null, "bar", ctx).pass); + assertTrue(Script.matchNamed("response", null, "bar", ctx).pass); + assertTrue(Script.matchNamed("/", null, "bar", ctx).pass); } @Test @@ -316,8 +329,8 @@ public void testMatchXmlButUsingJsonPath() { ScriptContext ctx = getContext(); Document doc = XmlUtils.toXmlDoc("Billie25"); ctx.vars.put("myXml", doc); - Script.matchNamed("myXml/cat/scores/score[2]", null, "'5'", ctx); - Script.matchNamed("myXml.cat.scores.score[1]", null, "5", ctx); + assertTrue(Script.matchNamed("myXml/cat/scores/score[2]", null, "'5'", ctx).pass); + assertTrue(Script.matchNamed("myXml.cat.scores.score[1]", null, "5", ctx).pass); } @Test @@ -327,9 +340,9 @@ public void testMatchXmlRepeatedElements() { Document doc = XmlUtils.toXmlDoc(xml); ctx.vars.put(ScriptValueMap.VAR_RESPONSE, doc); ScriptValue response = ctx.vars.get(ScriptValueMap.VAR_RESPONSE); - Script.matchXmlPath(false, response, "/", "baz1baz2", ctx); - Script.matchXmlPath(false, response, "/foo/bar[2]", "baz2", ctx); - Script.matchXmlPath(false, response, "/foo/bar[1]", "'baz1'", ctx); + assertTrue(Script.matchXmlPath(MatchType.EQUALS, response, "/", "baz1baz2", ctx).pass); + assertTrue(Script.matchXmlPath(MatchType.EQUALS, response, "/foo/bar[2]", "baz2", ctx).pass); + assertTrue(Script.matchXmlPath(MatchType.EQUALS, response, "/foo/bar[1]", "'baz1'", ctx).pass); } @Test @@ -487,16 +500,16 @@ public void testEvalParamWithDot() { public void testMatchJsonArrayContains() { ScriptContext ctx = getContext(); Script.assign("foo", "{ bar: [1, 2, 3] }", ctx); - assertTrue(Script.matchNamed(false, "foo.bar", null, "[1 ,2, 3]", ctx).pass); - assertTrue(Script.matchNamed(true, "foo.bar", null, "[1]", ctx).pass); + assertTrue(Script.matchNamed(MatchType.EQUALS, "foo.bar", null, "[1 ,2, 3]", ctx).pass); + assertTrue(Script.matchNamed(MatchType.CONTAINS, "foo.bar", null, "[1]", ctx).pass); } @Test public void testMatchStringContains() { ScriptContext ctx = getContext(); Script.assign("foo", "'hello world'", ctx); - assertTrue(Script.matchNamed(true, "foo", null, "'hello'", ctx).pass); - assertFalse(Script.matchNamed(true, "foo", null, "'zoo'", ctx).pass); + assertTrue(Script.matchNamed(MatchType.CONTAINS, "foo", null, "'hello'", ctx).pass); + assertFalse(Script.matchNamed(MatchType.CONTAINS, "foo", null, "'zoo'", ctx).pass); } @Test diff --git a/karate-core/src/test/java/com/intuit/karate/syntax/syntax.feature b/karate-core/src/test/java/com/intuit/karate/syntax/syntax.feature index 9342e9f14..f77a28e60 100755 --- a/karate-core/src/test/java/com/intuit/karate/syntax/syntax.feature +++ b/karate-core/src/test/java/com/intuit/karate/syntax/syntax.feature @@ -291,4 +291,12 @@ Then match pdf == read('test.pdf') * match foo contains { bar:1, baz: 'hello' } # * match foo == { bar:1, baz: 'hello' } +# match each +* def data = { foo: [{ bar: 1, baz: 'a' }, { bar: 2, baz: 'b' }, { bar: 3, baz: 'c' }]} +* match each data.foo == { bar: '#number', baz: '#string' } +* match each data.foo contains { bar: '#number' } +* match each data.foo contains { bar: '#? _ != 4' } +* match each data.foo contains { baz: "#? _ != 'z'" } +* def isAbc = function(x) { return x == 'a' || x == 'b' || x == 'c' } +* match each data.foo contains { baz: '#? isAbc(_)' }