├── .dockerignore ├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── STEPS.md ├── cucumber-rest-bdd.gemspec ├── docker-compose.yml ├── features ├── errors.feature ├── functional.feature ├── grabs.feature ├── headers.feature ├── levels.feature ├── methods.feature ├── response.feature ├── status.feature ├── support │ └── env.rb └── types.feature ├── hooks └── build ├── lib ├── cucumber-rest-bdd.rb └── cucumber-rest-bdd │ ├── data.rb │ ├── hash.rb │ ├── level.rb │ ├── list.rb │ ├── steps.rb │ ├── steps │ ├── functional.rb │ ├── resource.rb │ ├── response.rb │ └── status.rb │ ├── types.rb │ └── url.rb ├── rubocop └── Dockerfile └── server ├── db.json ├── error.js └── routes.json /.dockerignore: -------------------------------------------------------------------------------- 1 | /features 2 | /server 3 | README.md 4 | STEPS.md 5 | LICENSE 6 | Dockerfile 7 | .travis.yml 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | *.bridgesupport 21 | build-iPhoneOS/ 22 | build-iPhoneSimulator/ 23 | 24 | ## Specific to RubyMotion (use of CocoaPods): 25 | # 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 29 | # 30 | # vendor/Pods/ 31 | 32 | ## Documentation cache and generated files: 33 | /.yardoc/ 34 | /_yardoc/ 35 | /doc/ 36 | /rdoc/ 37 | 38 | ## Environment normalization: 39 | /.bundle/ 40 | /vendor/bundle 41 | /lib/bundler/man/ 42 | 43 | # for a library or gem, you might want to ignore these files since the code is 44 | # intended to run in multiple environments; otherwise, check them in: 45 | # Gemfile.lock 46 | # .ruby-version 47 | # .ruby-gemset 48 | 49 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 50 | .rvmrc 51 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Metrics/LineLength: 2 | # This will disable the rule completely, regardless what other options you put 3 | Enabled: true 4 | # Change the default 80 chars limit value 5 | Max: 120 6 | 7 | Metrics/AbcSize: 8 | Severity: refactor 9 | 10 | Metrics/MethodLength: 11 | Severity: refactor 12 | 13 | Metrics/CyclomaticComplexity: 14 | Severity: refactor 15 | 16 | Metrics/PerceivedComplexity: 17 | Severity: refactor 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: shell 2 | sudo: required 3 | dist: trusty 4 | group: edge 5 | services: 6 | - docker 7 | install: 8 | - make build 9 | script: 10 | - make test 11 | - make lint 12 | deploy: 13 | - provider: rubygems 14 | api_key: 15 | secure: obDis+hzVaV8Dk0i2V9bd3QiFhjdEpMZ6SxmTWDnbLcnYuhJyY7VPO3Jk9hINAWZPxH//uByVT+5p2r3YpmKUQxxoMtqJ7R8+aybfvlrDC9ZNoyE6lYF/Ysnj03pXMA5qwfqXt5ZVZuMvqAUj8J3sQBRSFwq7f/tlsAQdG1nowPL9lkxqOE++z+WvMvzkTs4x2RosrBg81eFNiCe0XKUXQGFbIneUA34Xjf/Mg+dls0UckyKBJO/3yoq3aCjYAaMGeA0UtfXwVqBdv7xhv8edzuJwbwrnstgQmwk6pR6XVIPff8wqHvRMHRXcFlPvRYv9WW4/CIxi6nvdpSlgeVXs9q1I22Z7f5sJy0As41wLoG95NVokqb2tFRz7ZwZU6tFNsJvK2tO8Bas0Gx6KTTThb2NTNdWQFczYBI/ERtYUB9qVRVlPDsK+iLRmFLi71H50aSbwnuXGEAfinXRmXvpLgPe1ZMYxMq+uz/ETBpCBfD26FQm3ieXIEb92GG7eswbIyv/LktCglEhNXR6FImqf1Ni2OOSXskJH+DTGnOkDHVFq5AhGQ8MtvJdsnJdm4p3PKzddzMPJ58cTwDOEd4uq7uNR/PGtE1w5FE3FNVIXr5ny1f9CpDwMpLL9xQ/AbgV9bJxBzqseBvXhn3HondJyZld1rfiUopvz/XGrydRvAo= 16 | gem: cucumber-rest-bdd 17 | on: 18 | branch: master 19 | repo: graze/cucumber-rest-bdd 20 | - provider: rubygems 21 | api_key: 22 | secure: obDis+hzVaV8Dk0i2V9bd3QiFhjdEpMZ6SxmTWDnbLcnYuhJyY7VPO3Jk9hINAWZPxH//uByVT+5p2r3YpmKUQxxoMtqJ7R8+aybfvlrDC9ZNoyE6lYF/Ysnj03pXMA5qwfqXt5ZVZuMvqAUj8J3sQBRSFwq7f/tlsAQdG1nowPL9lkxqOE++z+WvMvzkTs4x2RosrBg81eFNiCe0XKUXQGFbIneUA34Xjf/Mg+dls0UckyKBJO/3yoq3aCjYAaMGeA0UtfXwVqBdv7xhv8edzuJwbwrnstgQmwk6pR6XVIPff8wqHvRMHRXcFlPvRYv9WW4/CIxi6nvdpSlgeVXs9q1I22Z7f5sJy0As41wLoG95NVokqb2tFRz7ZwZU6tFNsJvK2tO8Bas0Gx6KTTThb2NTNdWQFczYBI/ERtYUB9qVRVlPDsK+iLRmFLi71H50aSbwnuXGEAfinXRmXvpLgPe1ZMYxMq+uz/ETBpCBfD26FQm3ieXIEb92GG7eswbIyv/LktCglEhNXR6FImqf1Ni2OOSXskJH+DTGnOkDHVFq5AhGQ8MtvJdsnJdm4p3PKzddzMPJ58cTwDOEd4uq7uNR/PGtE1w5FE3FNVIXr5ny1f9CpDwMpLL9xQ/AbgV9bJxBzqseBvXhn3HondJyZld1rfiUopvz/XGrydRvAo= 23 | gem: cucumber-rest-bdd 24 | on: 25 | tags: true 26 | repo: graze/cucumber-rest-bdd 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.5-alpine as build 2 | 3 | COPY . /build 4 | WORKDIR /build 5 | 6 | RUN set +xe \ 7 | && apk add --no-cache --virtual .ruby-builddeps \ 8 | autoconf \ 9 | bison \ 10 | bzip2 \ 11 | bzip2-dev \ 12 | ca-certificates \ 13 | coreutils \ 14 | dpkg-dev dpkg \ 15 | gcc \ 16 | g++ \ 17 | gdbm-dev \ 18 | glib-dev \ 19 | libc-dev \ 20 | libffi-dev \ 21 | libressl \ 22 | libressl-dev \ 23 | libxml2-dev \ 24 | libxslt-dev \ 25 | linux-headers \ 26 | make \ 27 | ncurses-dev \ 28 | procps \ 29 | readline-dev \ 30 | ruby \ 31 | tar \ 32 | xz \ 33 | yaml-dev \ 34 | zlib-dev \ 35 | && gem build cucumber-rest-bdd.gemspec \ 36 | && gem install cucumber-rest-bdd-*.gem \ 37 | && apk del .ruby-builddeps 38 | 39 | FROM ruby:2.5-alpine as app 40 | 41 | COPY --from=build /usr/local/bundle /usr/local/bundle 42 | 43 | WORKDIR /app 44 | 45 | ENV field_separator=_ \ 46 | field_camel=false \ 47 | resource_single=false \ 48 | cucumber_api_verbose=false \ 49 | data_key= \ 50 | error_key=error \ 51 | set_parent_id=false 52 | 53 | ARG BUILD_DATE 54 | ARG VCS_REF 55 | 56 | LABEL org.label-schema.schema-version="1.0" \ 57 | org.label-schema.vendor="graze" \ 58 | org.label-schema.name="cucumber-rest-bdd" \ 59 | org.label-schema.description="behavioural testing for REST apis" \ 60 | org.label-schema.vcs-url="https://github.com/graze/cucumber-rest-bdd" \ 61 | org.label-schema.version="0.6.1" \ 62 | org.label-schema.vcs-ref=$VCS_REF \ 63 | org.label-schema.build-date=$BUILD_DATE \ 64 | maintainer="harry.bragg@graze.com" \ 65 | version="0.6.1" \ 66 | license="MIT" 67 | 68 | ENTRYPOINT ["cucumber"] 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 graze.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TAG := graze/cucumber-rest-bdd 2 | RUN := docker run --rm -it -v $(PWD):/opt/src -w /opt/src ${TAG} 3 | 4 | build: 5 | docker-compose build runner 6 | 7 | lint: 8 | docker-compose run --rm rubocop --fail-level warning /src 9 | 10 | lint-fix: 11 | docker-compose run --rm rubocop --auto-correct /src 12 | 13 | test: start-test-server 14 | docker-compose run --rm runner --order random --format progress --fail-fast 15 | make stop-test-server > /dev/null 2>&1 & 16 | 17 | start-test-server: stop-test-server 18 | docker-compose up -d test-server 19 | 20 | stop-test-server: 21 | docker-compose stop test-server 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cucumber Rest BDD 2 | 3 | [![Build Status](https://travis-ci.org/graze/cucumber-rest-bdd.svg?branch=master)](https://travis-ci.org/graze/cucumber-rest-bdd) 4 | [![image](https://images.microbadger.com/badges/image/graze/cucumber-rest-bdd.svg)](https://microbadger.com/images/graze/cucumber-rest-bdd "Get your own image badge on microbadger.com") 5 | [![image version](https://images.microbadger.com/badges/version/graze/cucumber-rest-bdd.svg)](https://microbadger.com/images/graze/cucumber-rest-bdd "Get your own version badge on microbadger.com") 6 | [![image license](https://images.microbadger.com/badges/license/graze/cucumber-rest-bdd.svg)](https://microbadger.com/images/graze/cucumber-rest-bdd "Get your own license badge on microbadger.com") 7 | [![Gem Version](https://badge.fury.io/rb/cucumber-rest-bdd.svg)](https://badge.fury.io/rb/cucumber-rest-bdd) 8 | 9 | A set of Behavioural tests that can be run against a REST API. 10 | 11 | ![Giphy](https://media3.giphy.com/media/Tv7VPg6Os488g/giphy.gif) 12 | 13 | This is based from: [Effective API Testing With Cucumber](https://github.com/gregbeech/website/blob/master/blog/2014/effective-api-testing-with-cucumber.markdown) 14 | 15 | A list of [Steps](STEPS.md) shows the comparison between Behavioural and Functional tests provided by this package. 16 | 17 | ## Usage 18 | 19 | You can include this as a gem in your features, or run directly through docker 20 | 21 | **Gem:** 22 | 23 | ```bash 24 | ~$ gem install cucumber-rest-bdd 25 | ``` 26 | 27 | **Docker:** 28 | 29 | ```bash 30 | ~$ docker run --rm -v $(pwd):/opt/src -e endpoint=http://server/ graze/cucumber-rest-bdd 31 | ``` 32 | 33 | ## Configuration 34 | 35 | The following environment variables modify how this will operate: 36 | 37 | - `endpoint` - (string) the base url to call for each request 38 | - `data_key` - (string) the root data key (if applicable) (for example: `"data"` if all responses have a `{"data":{}}` field) 39 | - `error_key` - (string) this will ignore the `data_key` when checking for errors 40 | - `field_separator` - (string) the separator used between words by the api 41 | - `field_camel` - (bool [`true`|`false`]) does this endpoint use camelCase for fields (default: `false`) 42 | - `resource_single` - (bool [`true`|`false`]) if each resource should be singularized or not (default: `false`) 43 | - `set_parent_id` - (bool [`true`|`false`]) when creating sub resources, automatically add parent ids 44 | 45 | ## Examples 46 | 47 | - For a full list of steps see: [STEPS](STEPS.md) 48 | - These examples are taken from the test [features](features) 49 | 50 | ### Retrieve items 51 | 52 | ```gherkin 53 | Given I am a client 54 | When I request the post "1" 55 | Then the request was successful 56 | And the response has the following attributes: 57 | | attribute | type | value | 58 | | User Id | numeric | 1 | 59 | | Id | numeric | 1 | 60 | | Title | string | sunt aut facere repellat provident occaecati excepturi optio reprehenderit | 61 | | Body | string | quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto | 62 | ``` 63 | 64 | ```gherkin 65 | Given I am a client 66 | When I request the photo "1" for album "1" for user "1" 67 | Then the request was successful 68 | And the response has the attributes: 69 | | attribute | type | value | 70 | | title | string | accusamus beatae ad facilis cum similique qui sunt | 71 | ``` 72 | 73 | ```gherkin 74 | Given I am a client 75 | When I request a list of posts with: 76 | | User Id | 2 | 77 | Then the request is successful 78 | And the response is a list of at least 2 posts 79 | And one response has the following attributes: 80 | | attribute | type | value | 81 | | User Id | numeric | 2 | 82 | | Id | numeric | 11 | 83 | | Title | string | et ea vero quia laudantium autem | 84 | | Body | string | delectus reiciendis molestiae occaecati non minima eveniet qui voluptatibus\\naccusamus in eum beatae sit\\nvel qui neque voluptates ut commodi qui incidunt\nut animi commodi | 85 | ``` 86 | 87 | You can inspect child objects by using `:` in between the names 88 | 89 | ```gherkin 90 | Given I am a client 91 | When I request the comment "1" with: 92 | | `_expand` | post | 93 | Then the response has the following attributes: 94 | | attribute | type | value | 95 | | name | string | id labore ex et quam laborum | 96 | | email | string | Eliseo@gardner.biz | 97 | | body | string | laudantium enim quasi est quidem magnam voluptate ipsam eos\\ntempora quo necessitatibus\\ndolor quam autem quasi\\nreiciendis et nam sapiente accusantium | 98 | | post : title | string | sunt aut facere repellat provident occaecati excepturi optio reprehenderit | 99 | | post : body | string | quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto | 100 | ``` 101 | 102 | Alternatively you can inspect child arrays and objects by describing the path of the object with attributes 103 | 104 | ```gherkin 105 | Given I am a client 106 | When I set JSON request body to: 107 | """ 108 | {"title":"test","body":"multiple", 109 | "comments":[ 110 | {"common":1,"id":1,"title":"fish","body":"cake","image":{"href":"some_url"}}, 111 | {"common":1,"id":2,"title":"foo","body":"bar","image":{"href":"some_url"}} 112 | ]} 113 | """ 114 | And I send a POST request to "http://test-server/posts" 115 | Then the response has the attributes: 116 | | attribute | type | value | 117 | | title | string | test | 118 | | body | string | multiple | 119 | And the response has a list of comments 120 | And the response has a list of 2 comments 121 | And the response has two comments with attributes: 122 | | attribute | type | value | 123 | | common | integer | 1 | 124 | And the response has two comments with an image with attributes: 125 | | attribute | type | value | 126 | | href | string | some_url | 127 | And the response has one comment with attributes: 128 | | attribute | type | value | 129 | | Id | integer | 1 | 130 | | Title | string | fish | 131 | | Body | string | cake | 132 | And the response has one comment with attributes: 133 | | attribute | type | value | 134 | | Id | integer | 2 | 135 | | Title | string | foo | 136 | | Body | string | bar | 137 | ``` 138 | 139 | Each numeric request can be prefixed with a modifier to modify the number specified 140 | 141 | ```gherkin 142 | Given I am a client 143 | When I request a list of posts with: 144 | | `_embed` | comments | 145 | Then the response is a list of posts 146 | Then the response is a list of more than 5 posts 147 | Then the response is a list of at least 10 posts 148 | Then more than three posts have the attributes: 149 | | attribute | type | value | 150 | | User Id | integer | 5 | 151 | Then less than 200 posts have more than four comments 152 | Then more than 50 posts have less than six comments 153 | Then more than 80 posts have a list of comments 154 | Then at least 90 posts have a list of five comments 155 | Then more than 10 posts have five comments 156 | Then less than 200 posts have five comments 157 | ``` 158 | 159 | #### Errors 160 | 161 | If the `error_key` environment variable is set, if that key is used as the initial step it will ignore any `data_key` 162 | setting. 163 | 164 | Example: `error_key=error`, `data_key=data` 165 | 166 | ```gherkin 167 | Then the response has a list of posts | {"data":[{"id": 12}]} 168 | 169 | Then the response has one error | {"errors":[{"message": "Error"}]} 170 | 171 | Then the response has an error | {"error": {"message": "Error}} 172 | ``` 173 | 174 | Example: `error_key=`, `data_key=data` 175 | 176 | ```gherkin 177 | Then the response has a list of posts | {"data":[{"id": 12}]} 178 | 179 | Then the response has one error | {"data": {"errors":[{"message": "Error"}]}} 180 | 181 | Then the response has an error | {"data": {"error": {"message": "Error}}} 182 | ``` 183 | 184 | ### Creation 185 | 186 | ```gherkin 187 | Given I am a client 188 | When I request to create a post with: 189 | | attribute | type | value | 190 | | Title | string | foo | 191 | | Body | string | bar | 192 | | User Id | numeric | 1 | 193 | Then the request is successful and a post was created 194 | And the response has the following attributes: 195 | | attribute | type | value | 196 | | User Id | numeric | 1 | 197 | | Title | string | foo | 198 | | Body | string | bar | 199 | ``` 200 | 201 | If the environment variable: `set_parent_id` is set to `true` then when you create sub resources it will add the level aboves id into the json, otherwise it will rely on the api to do it for you 202 | 203 | ```gherkin 204 | Given I am a client 205 | When I request to create a photo in album "2" for user "1" with: 206 | | attribute | type | value | 207 | | title | string | foo | 208 | Then the comment was created 209 | And the response has the attributes: 210 | | attribute | type | value | 211 | | Album Id | int | 2 | 212 | | Title | string | foo | 213 | When I request a list of photos for album "2" for user "1" 214 | Then the request was successful 215 | And one comment has the attributes: 216 | | attribute | type | value | 217 | | Title | string | foo | 218 | ``` 219 | 220 | ### Removal 221 | 222 | ```gherkin 223 | Given I am a client 224 | When I request to remove the post "20" 225 | Then the request is successful 226 | ``` 227 | 228 | ### Modification 229 | 230 | ```gherkin 231 | Given I am a client 232 | When I request to modify the post "21" with: 233 | | attribute | type | value | 234 | | Title | string | foo | 235 | Then the request is successful 236 | And the response has the following attributes: 237 | | attribute | type | value | 238 | | User Id | numeric | 3 | 239 | | Title | string | foo | 240 | | Body | string | repellat aliquid praesentium dolorem quo\\nsed totam minus non itaque\\nnihil labore molestiae sunt dolor eveniet hic recusandae veniam\\ntempora et tenetur expedita sunt | 241 | ``` 242 | 243 | ### Multiple Requests 244 | 245 | ```gherkin 246 | Given I am a client 247 | When I request to create a post with: 248 | | attribute | type | value | 249 | | Title | string | foo | 250 | | Body | string | bar | 251 | | User Id | integer | 12 | 252 | Then the request is successful 253 | When I save "id" 254 | And I request the post "{id}" 255 | Then the request is successful 256 | And the response has the following attributes: 257 | | attribute | type | value | 258 | | Title | string | foo | 259 | | Body | string | bar | 260 | | User Id | numeric | 12 | 261 | | Id | numeric | {id} | 262 | ``` 263 | 264 | ## Resources 265 | 266 | A resource "name" is attempted to be retrieved from the given name of the item to be retrieved. This pluralises, ensures everything is lower case, removes any unparameterisable characters and uses a `-` separator. 267 | 268 | ```text 269 | Token -> tokens 270 | User -> users 271 | Big Life -> big-lives 272 | octopus -> octopi 273 | ``` 274 | 275 | If the environment variable: `resource_single` is set to `true` then it will not attempt to pluralise the resources. 276 | 277 | ```text 278 | Token -> token 279 | User -> user 280 | ``` 281 | 282 | You can directly pass what you want using: 283 | 284 | ```text 285 | `field` 286 | ``` 287 | 288 | this will not modify the field. 289 | 290 | ## Attributes 291 | 292 | ### Types 293 | 294 | Attribute types: 295 | The following types are supported: 296 | 297 | | type | other names | example | 298 | |---------|-----------------------|-----------| 299 | | integer | numeric, number, long | 12 | 300 | | float | double, decimal | 4.8 | 301 | | string | text | "text" | 302 | | array | array | ["a"] | 303 | | object | object | {"a":"b"} | 304 | | null | nil | | 305 | | bool | boolean | true | 306 | 307 | ### Name conversion 308 | 309 | attributes are converted into singular parametrised versions of the provided name: 310 | 311 | The conversion is based on the provided environment variables `field_camel` and `field_separator` 312 | 313 | #### Default 314 | 315 | ```text 316 | field_camel=false 317 | field_separator=_ 318 | 319 | Someid -> someid 320 | Product Id -> product_id 321 | Bodies -> body 322 | ``` 323 | 324 | #### CamelCase 325 | 326 | ```text 327 | field_camel=true 328 | field_separator=_ 329 | 330 | Someid -> someid 331 | Product Id -> productId 332 | Bodies -> body 333 | ``` 334 | 335 | #### Other Separators 336 | 337 | ```text 338 | field_camel=false 339 | field_separator=- 340 | 341 | Someid -> someid 342 | Product Id -> product-id 343 | Bodies -> body 344 | ``` 345 | -------------------------------------------------------------------------------- /STEPS.md: -------------------------------------------------------------------------------- 1 | # Gherkin Steps 2 | 3 | This test suite introduces behavioural test steps on top of functional REST API steps from [cucumber-api](https://github.com/hidroh/cucumber-api) 4 | 5 | The following is a list of steps, and their equivalent functional step 6 | 7 | ## Setup 8 | 9 | ```text 10 | Behavioural Functional 11 | --------------------------------------------------- -------------------------------------------------------------- 12 | Given I am a client Given I send and accept JSON 13 | ``` 14 | 15 | ## Retrieval 16 | 17 | ```text 18 | Behavioural Functional 19 | --------------------------------------------------- -------------------------------------------------------------- 20 | When I request an item "2" When I send a GET request to "http://url/items/2" 21 | 22 | When I request a list of items When I send a GET request to "http://url/items" 23 | 24 | When I request a list of items with: When I send a GET request to "http://url/items" with: 25 | | User Id | 12 | | userId | 26 | | 12 | 27 | ``` 28 | 29 | ## Creation 30 | 31 | ```text 32 | Behavioural Functional 33 | --------------------------------------------------- -------------------------------------------------------------- 34 | When I request to create an item When I send a POST request to "http://url/items" 35 | 36 | When I request to create an item with: When I set JSON request body to: 37 | | attribute | type | value | """ 38 | | User Id | integer | 12 | {"userId":12,"title":"foo"} 39 | | Title | string | foo | """ 40 | And I send a POST request to "http://url/items" 41 | 42 | When I request to create an item with id "4" When I send a PUT request to "http://url/items/4" 43 | 44 | When I request to replace the item "4" with: When I set JSON request body to: 45 | | attribute | type | value | """ 46 | | User Id | integer | 7 | {"userId":7,"title":"foo"} 47 | | Title | string | foo | """ 48 | And I send a PUT request to "http://url/items/4" 49 | ``` 50 | 51 | ## Modification 52 | 53 | ```text 54 | Behavioural Functional 55 | --------------------------------------------------- -------------------------------------------------------------- 56 | When I request to modify the item "4" with: When I set JSON request body to: 57 | | attribute | type | value | """ 58 | | Body | string | bar | {"body":"bar"} 59 | """ 60 | And I send a PATCH request to "http://url/items/4" 61 | ``` 62 | 63 | ## Status Inspection 64 | 65 | ```text 66 | Behavioural Functional 67 | --------------------------------------------------- -------------------------------------------------------------- 68 | Then the request is successful Then the response status should be "200" 69 | 70 | Then the request was redirected (response status between "300" and "400") 71 | 72 | Then the request failed (response status between "400" and "600") 73 | 74 | Then the request was successful and an item was Then the response status should be "201" 75 | created 76 | 77 | Then the request was successfully accepted Then the response status should be "202" 78 | 79 | Then the request was successful and no response Then the response status should be "204" 80 | body is returned 81 | 82 | Then the request failed because it was invalid Then the response status should be "400" 83 | 84 | Then the request failed because I am unauthorised Then the response status should be "401" 85 | 86 | Then the request failed because it was forbidden Then the response status should be "403" 87 | 88 | Then the request failed because the item was not Then the response status should be "404" 89 | found 90 | 91 | Then the request failed because it was not allowed Then the response status should be "405" 92 | 93 | Then the request failed because there was a Then the response status should be "409" 94 | conflict 95 | 96 | Then the request failed because the item has gone Then the response status should be "410" 97 | 98 | Then the request failed because it was not Then the response status should be "501" 99 | implemented 100 | ``` 101 | 102 | ## Response Inspection 103 | 104 | ```text 105 | Behavioural Functional 106 | --------------------------------------------------- -------------------------------------------------------------- 107 | Then the response has the following attributes: Then the JSON response should have "userId" of type numeric 108 | | attribute | type | value | with value "12" 109 | | User Id | integer | 12 | Then the JSON response should have "title" of type numeric 110 | | Title | string | foo | with value "foo" 111 | | Body | string | bar | Then the JSON response should have "body" of type numeric with 112 | value "bar" 113 | 114 | Then the response is a list of 12 items Then the JSON response should have "$." of type array with 12 115 | entries 116 | 117 | Then the response is a list of at least 12 items Then the JSON response should have "$." of type array with at 118 | least 12 entries 119 | Then the response is a list of at most 12 items 120 | Then the response is a list of fewer than 12 items 121 | Then the response is a list of more than 12 items 122 | 123 | Then two items have have the following attributes: 124 | | attribute | type | value | 125 | | User Id | integer | 12 | 126 | | Title | string | foo | 127 | | Body | string | bar | 128 | 129 | Then more than two items have have the following 130 | attributes: 131 | | attribute | type | value | 132 | | User Id | integer | 12 | 133 | | Title | string | foo | 134 | | Body | string | bar | 135 | 136 | Then the JSON response should follow "schema.json" 137 | 138 | Then the response has the header "Content Type" with value 139 | "application/json" 140 | ``` 141 | 142 | ### Error Handling 143 | 144 | Using the environment variable: `error_key` to represent the error resource 145 | 146 | ```text 147 | Behavioural Functional 148 | --------------------------------------------------- -------------------------------------------------------------- 149 | Then the response has one error: Then the JSON response should have "errors[0].code" of type 150 | | attribute | type | value | string with value "ERR-BLA" 151 | | code | string | ERR-BLA | 152 | 153 | Then the response has one error with attributes: Then the JSON response should have "errors[0].code" of type 154 | | attribute | type | value | string with value "ERR-BLA" 155 | | code | string | ERR-BLA | 156 | 157 | Then the response has at least one error Then the JSON response should have "errors" of type array 158 | with at least 1 entry 159 | 160 | Then the response has an error Then the JSON response should have required key "error" of 161 | Then the response contains an error type object 162 | 163 | Then the response has two errors with: 164 | | attribute | type | value | 165 | | message | string | super error | 166 | 167 | Then the response has three errors with two links 168 | with: 169 | | attribute | type | value | 170 | | href | string | http://oops | 171 | ``` 172 | 173 | ### Attribute saving and re-use 174 | 175 | ```text 176 | Behavioural Functional 177 | --------------------------------------------------- -------------------------------------------------------------- 178 | When I save "User Id" as "user" When I grab "$.userId" as "user" 179 | And I request the user "{user}" And I send a GET request to "http://url/users/{user}" 180 | ``` 181 | 182 | ### Nested requests 183 | 184 | ```text 185 | Behavioural Functional 186 | --------------------------------------------------- -------------------------------------------------------------- 187 | When I request a list of comments for post "1" When I send a GET request to "http://url/posts/1/comments" 188 | 189 | When I request the comment "2" for post "3" When I send a GET request to "http://url/posts/3/comments/2" 190 | 191 | When I request the photo "3" in album "4" for user When I send a GET request to 192 | "5" "http://url/users/5/albums/4/photos/3" 193 | 194 | When I request a list of photos in album "6" for When I send a GET request to 195 | user "7" "http://url/users/7/albums/6/photos" 196 | 197 | When I request to create a comment for post "8" When I send a POST request to "http://url/posts/8/comments" 198 | 199 | When I request to modify the comment "9" for post When I send a PATCH request to 200 | "10" "http://url/posts/10/comments/9" 201 | 202 | When I request to set photo "11" in album "12" to: When I set JSON request body to: 203 | | attribute | type | value | """ 204 | | url | string | http://url/image.jpg | {"url":"http://url/image.jpg"} 205 | """ 206 | And I send a PUT request to "http://url/albums/12/photos/11" 207 | ``` 208 | 209 | ### Nested responses 210 | 211 | ```text 212 | Behavioural Functional 213 | --------------------------------------------------- -------------------------------------------------------------- 214 | Then the response has the following attributes: Then the JSON response should have "userId" of type numeric 215 | | attribute | type | value | with value "12" 216 | | User Id | integer | 12 | Then the JSON response should have "title" of type numeric 217 | | Title | string | foo | with value "foo" 218 | | Body | string | bar | Then the JSON response should have "body" of type numeric with 219 | | Post : Title | string | baz | value "bar" 220 | | Post : Body | string | boo | Then the JSON response should have "post.title" of type string 221 | with value "baz" 222 | Then the JSON response should have "post.body" of type string 223 | with value "boo" 224 | 225 | Then the response has a list of comments Then the JSON response should have "comments" of type array 226 | 227 | Then the response has a list of 2 comments Then the JSON response should have "comments" of type array with 228 | 2 entries 229 | Then the response has a list of at least Then the JSON response should have "comments" of type array with 230 | 2 comments at least 2 entries 231 | 232 | Then the response has a post with two comments 233 | with attributes: 234 | | attribute | type | value | 235 | | Title | string | foo | 236 | | Body | string | bar | 237 | 238 | Then two items contains two posts with three 239 | comments with an image with attributes: 240 | | attribute | type | value | 241 | | Href | string | some_url | 242 | 243 | Then more than two items contains fewer than two 244 | posts with at least three comments with an 245 | image with attributes: 246 | | attribute | type | value | 247 | | Href | string | some_url | 248 | 249 | Then the response has a post with a list of Then the JSON response should have "post.comments" of type array 250 | comments 251 | 252 | Then the response has a post with a list of more Then the JSON response should have "post.comments" of type array 253 | than 3 comments with at least 4 comments 254 | 255 | Then more than three posts have less than two 256 | comments 257 | ``` 258 | -------------------------------------------------------------------------------- /cucumber-rest-bdd.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'cucumber-rest-bdd' 3 | s.version = if ENV['TRAVIS'] && ENV['TRAVIS_TAG'] == '' 4 | "0.6.1-#{ENV['TAVIS_BUILD_NUMBER']}" 5 | else 6 | '0.6.1' 7 | end 8 | s.platform = Gem::Platform::RUBY 9 | s.date = '2018-07-09' 10 | s.summary = 'BDD Rest API specifics for cucumber' 11 | s.description = 'Series of BDD cucumber rules for testing API endpoints' 12 | s.authors = ['Graze Developers', 'Harry Bragg', 'Matt Hosking'] 13 | s.email = [ 14 | 'developers@graze.com', 15 | 'harry.bragg@graze.com', 16 | 'Matt.Hosking@alintaenergy.com.au' 17 | ] 18 | s.files = Dir['lib/**/*.rb'] 19 | s.require_paths = ['lib'] 20 | s.homepage = 'https://github.com/graze/cucumber-rest-bdd' 21 | s.license = 'MIT' 22 | 23 | s.add_dependency('activesupport', '~> 5.1') 24 | s.add_dependency('cucumber-api', '~> 0.6') 25 | s.add_dependency('cucumber-expressions', '~> 5.0', '>= 5.0.17') 26 | s.add_dependency('easy_diff', '~> 1.0') 27 | end 28 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | test-server: 4 | image: clue/json-server 5 | command: --routes /data/routes.json --middlewares /data/error.js 6 | volumes: 7 | - ./server:/data:ro 8 | 9 | runner: 10 | image: graze/cucumber-rest-bdd 11 | build: . 12 | volumes: 13 | - .:/opt/src 14 | working_dir: /opt/src 15 | environment: 16 | - endpoint=http://test-server/ 17 | - cucumber_api_verbose=false 18 | - field_separator=_ 19 | - field_camel=true 20 | - resource_single=false 21 | - data_key= 22 | - error_key=error 23 | - set_parent_id=true 24 | 25 | rubocop: 26 | build: rubocop/. 27 | volumes: 28 | - .:/src 29 | -------------------------------------------------------------------------------- /features/errors.feature: -------------------------------------------------------------------------------- 1 | @errors 2 | Feature: Handling error responses 3 | 4 | Background: 5 | Given I am a client 6 | 7 | Scenario: Handle errors as a list 8 | When I request the error "list" 9 | Then the request fails because the post was not found 10 | And the response contains at least one error 11 | And the response has one error 12 | And the response has one error with the attributes: 13 | | attribute | type | value | 14 | | Message | string | Not Found | 15 | 16 | Scenario: Handle error as a single item 17 | When I request the error "single" 18 | Then the request fails because it was invalid 19 | And the response contains an error 20 | And the response has an error 21 | And the response has an error with the attributes: 22 | | attribute | type | value | 23 | | Message | string | Bad Request | 24 | -------------------------------------------------------------------------------- /features/functional.feature: -------------------------------------------------------------------------------- 1 | Feature: Performing different rest methods 2 | 3 | Background: 4 | Given I am a client 5 | 6 | Scenario: Count the number of elements 7 | When I request a list of posts with: 8 | | User Id | 8 | 9 | Then the request is successful 10 | And the JSON response should have "$." of type array with at least 1 entry 11 | And the JSON response should have "$." of type array with at least 10 entries 12 | And the JSON response should have "$." of type array with 10 entries 13 | And the JSON response should have "$." of type array with at most 10 entries 14 | And the JSON response should have "$." of type array with at most 11 entries 15 | 16 | Scenario: Check for null type 17 | When I request to create a post with: 18 | | attribute | type | value | 19 | | title | string | foo | 20 | | body | text | bar | 21 | | null | null | | 22 | | nil | nil | | 23 | Then the JSON response should have "null" of type null 24 | Then the JSON response should have "nil" of type nil 25 | Then the JSON response should have "nil" of type nill 26 | -------------------------------------------------------------------------------- /features/grabs.feature: -------------------------------------------------------------------------------- 1 | Feature: Using the response from a previous request 2 | 3 | Background: 4 | Given I am a client 5 | 6 | Scenario: Get an id from creation and use in get 7 | When I request to create a post with: 8 | | attribute | type | value | 9 | | Title | string | foo | 10 | | Body | string | bar | 11 | | User Id | integer | 12 | 12 | Then the request is successful 13 | When I save "id" 14 | And I request the post "{id}" 15 | Then the request is successful 16 | And the response has the following attributes: 17 | | attribute | type | value | 18 | | Title | string | foo | 19 | | Body | string | bar | 20 | | User Id | numeric | 12 | 21 | | Id | numeric | {id} | 22 | -------------------------------------------------------------------------------- /features/headers.feature: -------------------------------------------------------------------------------- 1 | Feature: We can inspect the headers of the response 2 | 3 | Scenario: Parse a single result 4 | When I request the post "1" 5 | Then the request was successful 6 | And the response has the header "Content Type" with value "application/json; charset=utf-8" 7 | 8 | Scenario Outline: Can check for multiple headers 9 | When I request the post "1" 10 | Then the request was successful 11 | And the response has the header "
" with the value "" 12 | 13 | Examples: 14 | | header | value | 15 | | Content Type | application/json; charset=utf-8 | 16 | | Cache Control | no-cache | 17 | -------------------------------------------------------------------------------- /features/levels.feature: -------------------------------------------------------------------------------- 1 | Feature: Request multiple levels of REST api 2 | As a user 3 | I would like to query a url with multiple levels of depth 4 | So that I can get all the data from this api 5 | 6 | Background: 7 | Given I am a client 8 | 9 | Scenario: get single id from sub layer 10 | When I request the comment "1" for post "1" 11 | Then the request was successful 12 | And the response has the attributes: 13 | | attribute | type | value | 14 | | id | int | 1 | 15 | 16 | Scenario: get list from sub layer 17 | When I request a list of comments for post "1" 18 | Then the request was successful 19 | And the response is a list of more than 4 comments 20 | And one comment has the attributes: 21 | | attribute | type | value | 22 | | id | int | 1 | 23 | 24 | Scenario: multiple sub levels 25 | When I request the photo "1" for album "1" for user "1" 26 | Then the request was successful 27 | And the response has the attributes: 28 | | attribute | type | value | 29 | | title | string | accusamus beatae ad facilis cum similique qui sunt | 30 | 31 | Scenario: request a list of multiple sub levels 32 | When I request a list of photos for album "1" for user "1" 33 | Then the request was successful 34 | And the response is a list of more than 2 photos 35 | And one photo has the attributes: 36 | | attribute | type | value | 37 | | title | string | accusamus beatae ad facilis cum similique qui sunt | 38 | 39 | Scenario: create a item as a child 40 | When I request to create a comment for post "18" with: 41 | | attribute | type | value | 42 | | Title | string | foo | 43 | | Body | string | bar | 44 | Then the comment was created 45 | And the response has the attributes: 46 | | attribute | type | value | 47 | | Post Id | string | 18 | 48 | | Title | string | foo | 49 | | Body | string | bar | 50 | When I request a list of comments for post "18" 51 | Then the request was successful 52 | And one comment has the attributes: 53 | | attribute | type | value | 54 | | Title | string | foo | 55 | | Body | string | bar | 56 | 57 | Scenario: create a 2nd level child item 58 | When I request to create a photo in album "2" for user "1" with: 59 | | attribute | type | value | 60 | | title | string | foo | 61 | Then the comment was created 62 | And the response has the attributes: 63 | | attribute | type | value | 64 | | Album Id | int | 2 | 65 | | Title | string | foo | 66 | When I request a list of photos for album "2" for user "1" 67 | Then the request was successful 68 | And one comment has the attributes: 69 | | attribute | type | value | 70 | | Title | string | foo | 71 | 72 | Scenario: delete a child item 73 | When I request to delete the comment "91" for post "19" 74 | Then the request was successful 75 | When I request a list of comments for post "19" 76 | Then the request was successful 77 | And zero comments have the attributes: 78 | | attribute | type | value | 79 | | id | int | 91 | 80 | 81 | Scenario: modify a child item 82 | When I request to modify the comment "102" for post "21" with: 83 | | attribute | type | value | 84 | | Name | string | foo | 85 | | Body | string | bar | 86 | Then the request was successful 87 | And the response has the attributes: 88 | | attribute | type | value | 89 | | Post Id | int | 21 | 90 | | Name | string | foo | 91 | | Body | string | bar | 92 | When I request a list of comments for post "21" 93 | Then the request was successful 94 | And one comment has the attributes: 95 | | attribute | type | value | 96 | | Name | string | foo | 97 | | Body | string | bar | 98 | 99 | Scenario: modify a child item 100 | When I request to replace the comment "106" for post "22" with: 101 | | attribute | type | value | 102 | | Title | string | foo | 103 | | Body | string | bar | 104 | Then the request was successful 105 | And the response has the attributes: 106 | | attribute | type | value | 107 | | Post Id | int | 22 | 108 | | Title | string | foo | 109 | | Body | string | bar | 110 | When I request a list of comments for post "22" 111 | Then the request was successful 112 | And one comment has the attributes: 113 | | attribute | type | value | 114 | | Title | string | foo | 115 | | Body | string | bar | 116 | When I request the comment "106" 117 | Then the request was successful 118 | And the response has the attributes: 119 | | attribute | type | value | 120 | | Title | string | foo | 121 | | Body | string | bar | 122 | -------------------------------------------------------------------------------- /features/methods.feature: -------------------------------------------------------------------------------- 1 | Feature: Performing different rest methods 2 | 3 | Background: 4 | Given I am a client 5 | 6 | Scenario: Retrieve a single item 7 | When I request the post "1" 8 | Then the request was successful 9 | And the response has the following attributes: 10 | | attribute | type | value | 11 | | User Id | numeric | 1 | 12 | | Id | numeric | 1 | 13 | | Title | string | sunt aut facere repellat provident occaecati excepturi optio reprehenderit | 14 | | Body | string | quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto | 15 | 16 | Scenario: Retrieve multiple items 17 | When I request a list of posts 18 | Then the request was successful 19 | And the response is a list of at least 2 posts 20 | And one post has the following attributes: 21 | | attribute | type | value | 22 | | User Id | numeric | 1 | 23 | | Id | numeric | 1 | 24 | | Title | string | sunt aut facere repellat provident occaecati excepturi optio reprehenderit | 25 | | Body | string | quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto | 26 | And one post has the following attributes: 27 | | attribute | type | value | 28 | | User Id | numeric | 1 | 29 | | Id | numeric | 2 | 30 | | Title | string | qui est esse | 31 | | Body | string | est rerum tempore vitae\\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\\nqui aperiam non debitis possimus qui neque nisi nulla | 32 | And ten posts have the following attributes: 33 | | attribute | type | value | 34 | | User Id | numeric | 5 | 35 | And at most 20 posts have the following attributes: 36 | | attribute | type | value | 37 | | User Id | numeric | 5 | 38 | 39 | Scenario: Retrieve multiple items with filter 40 | When I request a list of posts with: 41 | | User Id | 2 | 42 | Then the request is successful 43 | And the response is a list of at least 2 posts 44 | And the response is a list of fewer than 100 posts 45 | And one post has the following attributes: 46 | | attribute | type | value | 47 | | User Id | numeric | 2 | 48 | | Id | numeric | 11 | 49 | | Title | string | et ea vero quia laudantium autem | 50 | | Body | string | delectus reiciendis molestiae occaecati non minima eveniet qui voluptatibus\\naccusamus in eum beatae sit\\nvel qui neque voluptates ut commodi qui incidunt\nut animi commodi | 51 | And one post has the following attributes: 52 | | attribute | type | value | 53 | | User Id | numeric | 2 | 54 | | Id | numeric | 12 | 55 | | Title | string | in quibusdam tempore odit est dolorem | 56 | | Body | string | itaque id aut magnam\\npraesentium quia et ea odit et ea voluptas et\\nsapiente quia nihil amet occaecati quia id voluptatem\\nincidunt ea est distinctio odio | 57 | And more than two posts has the following attributes: 58 | | attribute | type | value | 59 | | User Id | numeric | 2 | 60 | 61 | Scenario: Create an item 62 | When I request to create a post with: 63 | | attribute | type | value | 64 | | Title | string | foo | 65 | | Body | string | bar | 66 | | User Id | numeric | 1 | 67 | Then the request is successful and a post was created 68 | And the response has the following attributes: 69 | | attribute | type | value | 70 | | User Id | numeric | 1 | 71 | | Title | string | foo | 72 | | Body | string | bar | 73 | 74 | Scenario: Remove an item 75 | When I request to remove the post "20" 76 | Then the request is successful 77 | 78 | Scenario: Modify an item 79 | When I request to modify the post "21" with: 80 | | attribute | type | value | 81 | | Title | string | foo | 82 | Then the request is successful 83 | And the response has the following attributes: 84 | | attribute | type | value | 85 | | User Id | numeric | 3 | 86 | | Title | string | foo | 87 | | Body | string | repellat aliquid praesentium dolorem quo\\nsed totam minus non itaque\\nnihil labore molestiae sunt dolor eveniet hic recusandae veniam\\ntempora et tenetur expedita sunt | 88 | 89 | Scenario: Update an item with Id 90 | When I request to set a post "22" with: 91 | | attribute | type | value | 92 | | Title | string | foo | 93 | | Body | string | bar | 94 | | User Id | numeric | 1 | 95 | Then the request is successful 96 | And the response has the following attributes: 97 | | attribute | type | value | 98 | | User Id | numeric | 1 | 99 | | Id | numeric | 22 | 100 | | Title | string | foo | 101 | | Body | string | bar | 102 | -------------------------------------------------------------------------------- /features/response.feature: -------------------------------------------------------------------------------- 1 | Feature: Dealing with sub objects 2 | 3 | Background: 4 | Given I am a client 5 | 6 | Scenario: Create and read an item with child objects 7 | When I request to create a post with: 8 | | attribute | type | value | 9 | | Title | string | foo | 10 | | Body | string | bar | 11 | | User : Id | numeric | 1 | 12 | | User : name | string | name | 13 | Then the request is successful and a post was created 14 | And the response has the following attributes: 15 | | attribute | type | value | 16 | | Title | string | foo | 17 | | Body | string | bar | 18 | | User : Id | numeric | 1 | 19 | | User : name | string | name | 20 | 21 | Scenario: Request a single item with child objects 22 | When I request the comment "1" with: 23 | | `_expand` | post | 24 | Then the response has the following attributes: 25 | | attribute | type | value | 26 | | name | string | id labore ex et quam laborum | 27 | | email | string | Eliseo@gardner.biz | 28 | | body | string | laudantium enim quasi est quidem magnam voluptate ipsam eos\\ntempora quo necessitatibus\\ndolor quam autem quasi\\nreiciendis et nam sapiente accusantium | 29 | | post : title | string | sunt aut facere repellat provident occaecati excepturi optio reprehenderit | 30 | | post : body | string | quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto | 31 | 32 | Scenario: Request an item within a list of items 33 | When I request a list of comments with: 34 | | `_expand` | post | 35 | | Post ID | 1 | 36 | Then the response is a list of more than 1 item 37 | And the response is a list with at least one item with the following attributes: 38 | | attribute | type | value | 39 | | name | string | id labore ex et quam laborum | 40 | | email | string | Eliseo@gardner.biz | 41 | | body | string | laudantium enim quasi est quidem magnam voluptate ipsam eos\\ntempora quo necessitatibus\\ndolor quam autem quasi\\nreiciendis et nam sapiente accusantium | 42 | 43 | Scenario: Match an item with a list of items 44 | When I request the post "1" with: 45 | | `_embed` | comments | 46 | Then the response has the following attributes: 47 | | attribute | type | value | 48 | | title | string | sunt aut facere repellat provident occaecati excepturi optio reprehenderit | 49 | | body | string | quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto | 50 | And the response has 1 comment with attributes: 51 | | attribute | type | value | 52 | | name | string | id labore ex et quam laborum | 53 | | email | string | Eliseo@gardner.biz | 54 | | body | string | laudantium enim quasi est quidem magnam voluptate ipsam eos\\ntempora quo necessitatibus\\ndolor quam autem quasi\\nreiciendis et nam sapiente accusantium | 55 | And the response has five comments with attributes: 56 | | attribute | type | value | 57 | | Post Id | integer | 1 | 58 | And the response has at least five comments 59 | 60 | Scenario: Match a list of items within a list of items 61 | When I request a list of posts with: 62 | | `_embed` | comments | 63 | Then the response is a list with at least one item with the following attributes: 64 | | attribute | type | value | 65 | | title | string | sunt aut facere repellat provident occaecati excepturi optio reprehenderit | 66 | | body | string | quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto | 67 | And the response is a list with at least one item with 1 comment with attributes: 68 | | attribute | type | value | 69 | | name | string | id labore ex et quam laborum | 70 | | email | string | Eliseo@gardner.biz | 71 | | body | string | laudantium enim quasi est quidem magnam voluptate ipsam eos\\ntempora quo necessitatibus\\ndolor quam autem quasi\\nreiciendis et nam sapiente accusantium | 72 | And the response is a list with at least one item with 5 comments with attributes: 73 | | attribute | type | value | 74 | | Post Id | integer | 1 | 75 | And the response is a list with at least one item with at least five comments 76 | 77 | Scenario: Multiple levels 78 | When I request to create a post with: 79 | | attribute | type | value | 80 | | title | string | test | 81 | | body | string | multiple | 82 | | comments : [0] : common | integer | 1 | 83 | | comments : [0] : id | integer | 1 | 84 | | comments : [0] : title | string | fish | 85 | | comments : [0] : body | string | cake | 86 | | comments : [0] : image : href | string | some_url | 87 | | comments : [0] : image : name | string | some name | 88 | | comments : [1] : common | integer | 1 | 89 | | comments : [1] : id | integer | 2 | 90 | | comments : [1] : title | string | foo | 91 | | comments : [1] : body | string | bar | 92 | | comments : [1] : image : href | string | some_url | 93 | | comments : [1] : image : name | string | some other name | 94 | Then the response has the attributes: 95 | | attribute | type | value | 96 | | title | string | test | 97 | | body | string | multiple | 98 | And the response has a list of 2 comments 99 | And the response has two comments with attributes: 100 | | attribute | type | value | 101 | | common | integer | 1 | 102 | And the response has two comments have an image with the attributes: 103 | | attribute | type | value | 104 | | href | string | some_url | 105 | And the response has one comments have an image with the attributes: 106 | | attribute | type | value | 107 | | name | string | some name | 108 | And the response has one comments have an image with the attributes: 109 | | attribute | type | value | 110 | | name | string | some other name | 111 | And the response has one comment with the attributes: 112 | | attribute | type | value | 113 | | Id | integer | 1 | 114 | | Title | string | fish | 115 | | Body | string | cake | 116 | And the response has one comment has the attributes: 117 | | attribute | type | value | 118 | | Id | integer | 2 | 119 | | Title | string | foo | 120 | | Body | string | bar | 121 | 122 | Scenario: Multiple levels in array 123 | When I request a list of posts with: 124 | | `_embed` | comments | 125 | Then the request is successful 126 | And the response is a list with one post with one comment with attributes: 127 | | attribute | type | value | 128 | | Id | integer | 1 | 129 | And the response is a list with at least ten posts with five comments 130 | And the response is a list with more than 95 posts with more than four comments 131 | -------------------------------------------------------------------------------- /features/status.feature: -------------------------------------------------------------------------------- 1 | Feature: I can handle different response codes 2 | 3 | Background: 4 | Given I am a client 5 | 6 | Scenario: Successful response 7 | When I request the post "1" 8 | Then the request is successful 9 | 10 | Scenario: Key not found 11 | When I request the post "not-found" 12 | Then the request fails 13 | And the request fails because the post was not found 14 | 15 | Scenario: Resource not found 16 | When I request the Not Found "5" 17 | Then the request fails 18 | And the request fails because the not found was not found 19 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'cucumber-rest-bdd' 2 | -------------------------------------------------------------------------------- /features/types.feature: -------------------------------------------------------------------------------- 1 | Feature: Dealing with sub objects 2 | 3 | Background: 4 | Given I am a client 5 | 6 | Scenario: Handle null values 7 | When I request to create a post with: 8 | | attribute | type | value | 9 | | title | string | foo | 10 | | body | string | bar | 11 | | extra | null | | 12 | Then the request was successful 13 | And the response has the following attributes: 14 | | attribute | type | value | 15 | | title | string | foo | 16 | | body | string | bar | 17 | | extra | nil | | 18 | 19 | Scenario: Handle types 20 | When I request to create a post with: 21 | | attribute | type | value | 22 | | title | string | foo | 23 | | body | text | bar | 24 | | int | int | 1 | 25 | | long | long | 1347289473247823749 | 26 | | float | float | 1.2 | 27 | | double | double | 1.4 | 28 | | number | number | 12 | 29 | | numeric | numeric | 15 | 30 | | bool | bool | true | 31 | | boolean | boolean | false | 32 | | null | null | | 33 | | nil | nil | | 34 | Then the request was successful 35 | And the response has the following attributes: 36 | | attribute | type | value | 37 | | title | string | foo | 38 | | body | text | bar | 39 | | int | int | 1 | 40 | | long | long | 1347289473247824000 | 41 | | float | float | 1.2 | 42 | | double | double | 1.4 | 43 | | number | number | 12 | 44 | | numeric | numeric | 15 | 45 | | bool | bool | true | 46 | | boolean | boolean | false | 47 | | null | null | | 48 | | nil | nil | | 49 | -------------------------------------------------------------------------------- /hooks/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set +xe 4 | 5 | docker build --build-arg BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ 6 | --build-arg VCS_REF="$(git rev-parse --short HEAD)" \ 7 | -t "$IMAGE_NAME" . 8 | -------------------------------------------------------------------------------- /lib/cucumber-rest-bdd.rb: -------------------------------------------------------------------------------- 1 | require 'cucumber-rest-bdd/steps' 2 | -------------------------------------------------------------------------------- /lib/cucumber-rest-bdd/data.rb: -------------------------------------------------------------------------------- 1 | require 'cucumber-rest-bdd/types' 2 | 3 | # gets the relevant key for the response based on the first key element 4 | def get_key(grouping) 5 | error_key = ENV['error_key'] 6 | if error_key && !error_key.empty? && grouping.count > 1 && grouping[-2][:key].singularize == error_key 7 | '$.' 8 | else 9 | root_data_key 10 | end 11 | end 12 | 13 | # top level has 2 children 14 | # with an item containing 15 | # at most three fish with attributes: 16 | # 17 | # nesting = [ 18 | # {key=fish,count=3,count_mod='<=',type=multiple}, 19 | # {key=item,type=single}, 20 | # {key=children,type=multiple,count=2,count_mod='='}, 21 | # {root=true,type=single} 22 | # ] 23 | # 24 | # returns true if the expected data is contained within the data based on the 25 | # nesting information 26 | def nest_match_attributes(data, nesting, expected, match_value) 27 | # puts data.inspect, nesting.inspect, expected.inspect, match_value.inspect 28 | return false unless data 29 | return data.deep_include?(expected) if !match_value && nesting.empty? 30 | return data.include?(expected) if match_value && nesting.empty? 31 | 32 | local_nesting = nesting.dup 33 | level = local_nesting.pop 34 | child_data = get_child_data(level, data) 35 | 36 | nest_get_match(level, child_data, local_nesting, expected, match_value) 37 | end 38 | 39 | # nest_get_match returns true if the child data matches the expected data 40 | def nest_get_match(level, child_data, local_nesting, expected, match_value) 41 | case level[:type] 42 | when 'single' then 43 | nest_match_attributes(child_data, local_nesting, expected, match_value) 44 | when 'multiple' then 45 | child_check(level, child_data, local_nesting, expected, match_value) 46 | when 'list' then 47 | child_is_list(level, child_data) 48 | else 49 | raise %(Unknown nested data type: #{level[:type]}) 50 | end 51 | end 52 | 53 | # check that all the children in child_data match the expected values 54 | def child_check(level, child_data, local_nesting, expected, match_value) 55 | matched = child_data.select do |item| 56 | nest_match_attributes(item, local_nesting, expected, match_value) 57 | end 58 | level[:comparison].compare(matched.count) 59 | end 60 | 61 | # is the child a list, and does it match the comparison? 62 | def child_is_list(level, child_data) 63 | child_data.is_a?(Array) \ 64 | && (!level.key?(:comparison) \ 65 | || level[:comparison].compare(child_data.count)) 66 | end 67 | 68 | # parse the field and get the data for a given child 69 | def get_child_data(level, data) 70 | return data.dup if level[:root] 71 | 72 | level_key = case level[:type] 73 | when 'single' then parse_field(level[:key]) 74 | when 'multiple', 'list' then parse_list_field(level[:key]) 75 | end 76 | raise %(Key not found: #{level[:key]} as #{level_key} in #{data}) \ 77 | if data.is_a?(Array) || !data[level_key] 78 | 79 | data[level_key] 80 | end 81 | -------------------------------------------------------------------------------- /lib/cucumber-rest-bdd/hash.rb: -------------------------------------------------------------------------------- 1 | require 'easy_diff' 2 | 3 | # Adds deep_include? to the Hash class 4 | class Hash 5 | def deep_include?(other) 6 | diff = other.easy_diff(self) 7 | diff[0].delete_if { |_k, v| v.empty? if v.is_a?(::Hash) } 8 | diff[0].empty? 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/cucumber-rest-bdd/level.rb: -------------------------------------------------------------------------------- 1 | require 'cucumber-rest-bdd/types' 2 | require 'active_support/inflector' 3 | 4 | ParameterType( 5 | name: 'levels', 6 | regexp: /((?: (?:for|in|on) (?:#{RESOURCE_NAME_SYNONYM})(?: with (?:key|id))? "[^"]*")*)/, 7 | transformer: ->(levels) { Level.new(levels) }, 8 | use_for_snippets: false 9 | ) 10 | 11 | # Helper class when creating nested resources 12 | class Level 13 | @urls = [] 14 | 15 | def initialize(levels) 16 | arr = [] 17 | while (matches = /^ (?:for|in|on) ([^"]+?)(?: with (?:key|id))? "([^"]*)"/ 18 | .match(levels)) 19 | levels = levels[matches[0].length, levels.length] 20 | item = { resource: get_resource(matches[1]), id: matches[2] } 21 | item[:id] = item[:id].to_i if item[:id] =~ /^\d+$/ 22 | arr.append(item) 23 | end 24 | @urls = arr.reverse 25 | end 26 | 27 | def url 28 | @urls.map { |l| "#{l[:resource]}/#{l[:id]}/" }.join 29 | end 30 | 31 | def hash 32 | hash = {} 33 | @urls.each do |l| 34 | hash[parse_field("#{parse_field(l[:resource]).singularize}_id")] = l[:id] 35 | end 36 | hash 37 | end 38 | 39 | def last_hash 40 | last = @urls.last 41 | unless last.nil? 42 | key = parse_field("#{parse_field(last[:resource]).singularize}_id") 43 | return { 44 | key => last[:id] 45 | } 46 | end 47 | {} 48 | end 49 | 50 | def to_s 51 | url 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/cucumber-rest-bdd/list.rb: -------------------------------------------------------------------------------- 1 | require 'cucumber-rest-bdd/types' 2 | 3 | HAVE_ALTERNATION = 'has/have/having/contain/contains/containing/with'.freeze 4 | FEWER_MORE_THAN_SYNONYM = '(?:fewer|less|more)\sthan|at\s(?:least|most)'.freeze 5 | INT_AS_WORDS_SYNONYM = 'zero|one|two|three|four|five|six|seven|eight|nine|ten' 6 | .freeze 7 | MAXIMAL_FIELD_NAME_SYNONYM = '\w+\b(?:(?:\s+:)?\s+\w+\b)*|`[^`]*`'.freeze 8 | 9 | ParameterType( 10 | name: 'list_has_count', 11 | regexp: /a|an|(?:(?:#{FEWER_MORE_THAN_SYNONYM})\s+)?(?:#{INT_AS_WORDS_SYNONYM}|\d+)/, 12 | transformer: lambda { |match| 13 | matches = /(?:(#{FEWER_MORE_THAN_SYNONYM})\s+)? 14 | (#{INT_AS_WORDS_SYNONYM}|\d+)/x.match(match) 15 | return ListCountComparison.new(matches[1], matches[2]) 16 | }, 17 | use_for_snippets: false 18 | ) 19 | 20 | ParameterType( 21 | name: 'list_nesting', 22 | # rubocop:disable Metrics/LineLength 23 | regexp: /(?:(?:#{HAVE_ALTERNATION.split('/').join('|')})?\s*(?:a list of\s+)?(?:a|an|(?:(?:#{FEWER_MORE_THAN_SYNONYM})\s+)?(?:#{INT_AS_WORDS_SYNONYM}|\d+))\s+(?:#{FIELD_NAME_SYNONYM})\s*)+/, 24 | # rubocop:enable Metrics/LineLength 25 | transformer: ->(match) { ListNesting.new(match) }, 26 | use_for_snippets: false 27 | ) 28 | 29 | # Handle many children within objects or lists 30 | class ListNesting 31 | def initialize(match) 32 | @match = match 33 | # gets an array in the nesting format that nest_match_attributes understands 34 | # to interrogate nested object and array data 35 | grouping = [] 36 | nesting = match 37 | 38 | minimal_list = /(?:#{HAVE_ALTERNATION.split('/').join('|')})?\s* 39 | (?:(a\slist\sof)\s+)?(?:a|an|(?:(#{FEWER_MORE_THAN_SYNONYM}) 40 | \s+)?(#{INT_AS_WORDS_SYNONYM}|\d+))\s+ 41 | (#{FIELD_NAME_SYNONYM})/x 42 | maximal_list = /(?:#{HAVE_ALTERNATION.split('/').join('|')})?\s* 43 | (?:(a\slist\sof)\s+)?(?:a|an|(?:(#{FEWER_MORE_THAN_SYNONYM}) 44 | \s+)?(#{INT_AS_WORDS_SYNONYM}|\d+))\s+ 45 | (#{MAXIMAL_FIELD_NAME_SYNONYM})/x 46 | while (matches = minimal_list.match(nesting)) 47 | next_matches = minimal_list.match(nesting[matches.end(0), nesting.length]) 48 | to_match = if next_matches.nil? 49 | nesting 50 | else 51 | nesting[0, matches.end(0) + next_matches.begin(0)] 52 | end 53 | matches = maximal_list.match(to_match) 54 | nesting = nesting[matches.end(0), nesting.length] 55 | 56 | level = if matches[1].nil? 57 | if matches[3].nil? 58 | { 59 | type: 'single', 60 | key: matches[4], 61 | root: false 62 | } 63 | else 64 | { 65 | type: 'multiple', 66 | key: matches[4], 67 | comparison: ListCountComparison.new(matches[2], matches[3]), 68 | root: false 69 | } 70 | end 71 | else 72 | { 73 | type: 'list', 74 | key: matches[4], 75 | comparison: ListCountComparison.new(matches[2], matches[3]), 76 | root: false 77 | } 78 | end 79 | grouping.push(level) 80 | end 81 | @grouping = grouping.reverse 82 | end 83 | 84 | def push(node) 85 | @grouping.push(node) 86 | end 87 | 88 | attr_reader :match 89 | 90 | attr_reader :grouping 91 | end 92 | 93 | # Store a value and a comparison operator to determine if a provided number 94 | # validates against it 95 | class ListCountComparison 96 | def initialize(type, amount) 97 | @type = type.nil? ? CMP_EQUALS : to_compare(type) 98 | @amount = amount.nil? ? 1 : to_num(amount) 99 | end 100 | 101 | def compare(actual) 102 | case @type 103 | when CMP_LESS_THAN then actual < @amount 104 | when CMP_MORE_THAN then actual > @amount 105 | when CMP_AT_MOST then actual <= @amount 106 | when CMP_AT_LEAST then actual >= @amount 107 | when CMP_EQUALS then actual == @amount 108 | else actual == @amount 109 | end 110 | end 111 | 112 | attr_reader :type 113 | 114 | def amount 115 | amount 116 | end 117 | 118 | # turn a comparison into a string 119 | def compare_to_string 120 | case @type 121 | when CMP_LESS_THAN then 'fewer than ' 122 | when CMP_MORE_THAN then 'more than ' 123 | when CMP_AT_LEAST then 'at least ' 124 | when CMP_AT_MOST then 'at most ' 125 | when CMP_EQUALS then 'exactly ' 126 | else '' 127 | end 128 | end 129 | 130 | def to_string 131 | compare_to_string + ' ' + @amount.to_s 132 | end 133 | end 134 | 135 | CMP_LESS_THAN = '<'.freeze 136 | CMP_MORE_THAN = '>'.freeze 137 | CMP_AT_LEAST = '>='.freeze 138 | CMP_AT_MOST = '<='.freeze 139 | CMP_EQUALS = '='.freeze 140 | 141 | # take a number modifier string (fewer than, less than, etc) and return an 142 | # operator '<', etc 143 | def to_compare(compare) 144 | case compare 145 | when 'fewer than' then CMP_LESS_THAN 146 | when 'less than' then CMP_LESS_THAN 147 | when 'more than' then CMP_MORE_THAN 148 | when 'at least' then CMP_AT_LEAST 149 | when 'at most' then CMP_AT_MOST 150 | else CMP_EQUALS 151 | end 152 | end 153 | 154 | def to_num(num) 155 | if num =~ /^(?:zero|one|two|three|four|five|six|seven|eight|nine|ten)$/ 156 | return %w[zero one two three four five six seven eight nine ten].index(num) 157 | end 158 | 159 | num.to_i 160 | end 161 | -------------------------------------------------------------------------------- /lib/cucumber-rest-bdd/steps.rb: -------------------------------------------------------------------------------- 1 | require 'cucumber-rest-bdd/steps/functional' 2 | require 'cucumber-rest-bdd/steps/response' 3 | require 'cucumber-rest-bdd/steps/resource' 4 | require 'cucumber-rest-bdd/steps/status' 5 | -------------------------------------------------------------------------------- /lib/cucumber-rest-bdd/steps/functional.rb: -------------------------------------------------------------------------------- 1 | require 'cucumber-api/response' 2 | require 'cucumber-api/steps' 3 | require 'cucumber-rest-bdd/types' 4 | require 'cucumber-rest-bdd/list' 5 | 6 | Then( 7 | 'the response (should )have/has (a/the )header {string} '\ 8 | "#{HAVE_ALTERNATION} (a/the )value {string}" 9 | ) do |header, value| 10 | p_value = resolve(value) 11 | p_header = header.parameterize 12 | unless @response.raw_headers.key?(p_header) 13 | raise %(Required header: #{header} not found 14 | #{@response.raw_headers.inspect}) 15 | end 16 | unless @response.raw_headers[p_header].include? p_value 17 | raise %/Expect #{p_value} in #{header} (#{p_header}) 18 | #{@response.raw_headers.inspect}/ 19 | end 20 | end 21 | 22 | Then( 23 | 'the JSON response should have {string} of type array '\ 24 | "#{HAVE_ALTERNATION} {list_has_count} entry/entries" 25 | ) do |json_path, list_comparison| 26 | list = @response.get_as_type json_path, 'array' 27 | unless list_comparison.compare(list.count) 28 | raise %(Expected #{list_comparison.to_string} entries in array for path 29 | '#{json_path}', found: #{list.count} 30 | #{@response.to_json_s}) 31 | end 32 | end 33 | 34 | Then( 35 | 'the JSON response should have {string} of type {word} that matches {string}' 36 | ) do |json_path, type, regex| 37 | value = @response.get_as_type json_path, type 38 | if (Regexp.new(regex) =~ value).nil? 39 | raise %(Expected #{json_path} value '#{value}' to match regex: #{regex} 40 | #{@response.to_json_s}) 41 | end 42 | end 43 | 44 | Then( 45 | 'the JSON response should have {string} of type nill/null/nil' 46 | ) do |json_path| 47 | value = @response.get_as_type_or_null json_path, 'string' 48 | unless value.nil? 49 | raise %(Expected #{json_path} to be nil, was: #{value.class} 50 | #{@response.to_json_s}) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/cucumber-rest-bdd/steps/resource.rb: -------------------------------------------------------------------------------- 1 | require 'cucumber-api/response' 2 | require 'cucumber-api/steps' 3 | require 'active_support/inflector' 4 | require 'cucumber-rest-bdd/url' 5 | require 'cucumber-rest-bdd/types' 6 | require 'cucumber-rest-bdd/level' 7 | require 'cucumber-rest-bdd/hash' 8 | require 'easy_diff' 9 | 10 | GET_TYPES = '(?:an?(?! list)|the)'.freeze 11 | WITH_ID = '(?: with (?:key|id))? "([^"]*)"'.freeze 12 | 13 | Given('I am a client') do 14 | steps %(Given I send "application/json" and accept JSON) 15 | end 16 | 17 | Given('I am issuing requests for {resource_name}') do |resource| 18 | @urlbasepath = resource 19 | end 20 | 21 | # GET 22 | 23 | When( 24 | 'I request the {resource_name} (with key/id ){string}{levels}' 25 | ) do |resource, id, levels| 26 | url = get_url("#{levels.url}#{resource}/#{id}") 27 | steps %(When I send a GET request to "#{url}") 28 | end 29 | 30 | When( 31 | 'I request the {resource_name} (with key/id ){string}{levels} with:' 32 | ) do |resource, id, levels, params| 33 | url = get_url("#{levels.url}#{resource}/#{id}") 34 | unless params.raw.empty? 35 | query = params.raw.map { |k, v| %(#{parse_field(k)}=#{resolve(v)}) } 36 | .join('&') 37 | url = "#{url}?#{query}" 38 | end 39 | steps %(When I send a GET request to "#{url}") 40 | end 41 | 42 | When('I request a list of {resource_name}{levels}') do |resource, levels| 43 | url = get_url("#{levels.url}#{resource}") 44 | steps %(When I send a GET request to "#{url}") 45 | end 46 | 47 | When( 48 | 'I request a list of {resource_name}{levels} with:' 49 | ) do |resource, levels, params| 50 | url = get_url("#{levels.url}#{resource}") 51 | unless params.raw.empty? 52 | query = params.raw.map { |k, v| %(#{parse_field(k)}=#{resolve(v)}) } 53 | .join('&') 54 | url = "#{url}?#{query}" 55 | end 56 | steps %(When I send a GET request to "#{url}") 57 | end 58 | 59 | # DELETE 60 | 61 | When( 62 | 'I request to delete/remove a/an/the {resource_name} '\ 63 | '(with key/id ){string}{levels}' 64 | ) do |resource, id, levels| 65 | url = get_url("#{levels.url}#{resource}/#{id}") 66 | steps %(When I send a DELETE request to "#{url}") 67 | end 68 | 69 | # POST 70 | 71 | When( 72 | 'I request to create a/an/the {resource_name}{levels}' 73 | ) do |resource, levels| 74 | if ENV['set_parent_id'] == 'true' 75 | json = MultiJson.dump(levels.last_hash) 76 | steps %( 77 | When I set JSON request body to: 78 | """ 79 | #{json} 80 | """ 81 | ) 82 | end 83 | url = get_url("#{levels.url}#{resource}") 84 | steps %(When I send a POST request to "#{url}") 85 | end 86 | 87 | When( 88 | 'I request to create a/an/the {resource_name}{levels} with:' 89 | ) do |resource, levels, params| 90 | request_hash = parse_attributes(params.hashes) 91 | request_hash = request_hash.merge(levels.last_hash) if ENV['set_parent_id'] == 'true' 92 | json = MultiJson.dump(request_hash) 93 | url = get_url("#{levels.url}#{resource}") 94 | steps %( 95 | When I set JSON request body to: 96 | """ 97 | #{json} 98 | """ 99 | And I send a POST request to "#{url}" 100 | ) 101 | end 102 | 103 | # PUT 104 | 105 | When( 106 | 'I request to replace/set a/an/the {resource_name} '\ 107 | '(with key/id ){string}{levels}' 108 | ) do |resource, id, levels| 109 | if ENV['set_parent_id'] == 'true' 110 | json = MultiJson.dump(levels.last_hash) 111 | steps %( 112 | When I set JSON request body to: 113 | """ 114 | #{json} 115 | """ 116 | ) 117 | end 118 | url = get_url("#{levels.url}#{resource}/#{id}") 119 | steps %( 120 | When I send a PUT request to "#{url}" 121 | ) 122 | end 123 | 124 | When( 125 | 'I request to replace/set a/an/the {resource_name} '\ 126 | '(with key/id ){string}{levels} with:' 127 | ) do |resource, id, levels, params| 128 | request_hash = parse_attributes(params.hashes) 129 | request_hash = request_hash.merge(levels.last_hash) if ENV['set_parent_id'] == 'true' 130 | json = MultiJson.dump(request_hash) 131 | url = get_url("#{levels.url}#{resource}/#{id}") 132 | steps %( 133 | When I set JSON request body to: 134 | """ 135 | #{json} 136 | """ 137 | And I send a PUT request to "#{url}" 138 | ) 139 | end 140 | 141 | # PATCH 142 | 143 | When( 144 | 'I request to modify/update a/an/the {resource_name} '\ 145 | '(with key/id ){string}{levels} with:' 146 | ) do |resource, id, levels, params| 147 | request_hash = parse_attributes(params.hashes) 148 | json = MultiJson.dump(request_hash) 149 | url = get_url("#{levels.url}#{resource}/#{id}") 150 | steps %( 151 | When I set JSON request body to: 152 | """ 153 | #{json} 154 | """ 155 | And I send a PATCH request to "#{url}" 156 | ) 157 | end 158 | 159 | # value capture 160 | 161 | When('I save (attribute ){string}') do |attribute| 162 | steps %(When I grab "#{json_path(attribute)}" as "#{attribute}") 163 | end 164 | 165 | When('I save (attribute ){string} to {string}') do |attribute, ref| 166 | steps %(When I grab "#{json_path(attribute)}" as "#{ref}") 167 | end 168 | -------------------------------------------------------------------------------- /lib/cucumber-rest-bdd/steps/response.rb: -------------------------------------------------------------------------------- 1 | require 'cucumber-rest-bdd/steps/resource' 2 | require 'cucumber-rest-bdd/types' 3 | require 'cucumber-rest-bdd/list' 4 | require 'cucumber-rest-bdd/data' 5 | 6 | ParameterType( 7 | name: 'item_name', 8 | regexp: /([\w\s]+?)/, 9 | transformer: ->(s) { s }, 10 | use_for_snippets: false 11 | ) 12 | 13 | Then('print the response') do 14 | puts %(The response:\n#{@response.to_json_s}) 15 | end 16 | 17 | # SIMPLE VALUE RESPONSE 18 | 19 | # response is a string with the specified value 20 | Then( 21 | "the response #{HAVE_ALTERNATION} (the )(following )value {string}" 22 | ) do |value| 23 | expected = value 24 | data = @response.get root_data_key 25 | raise %(Response did not match: #{expected}\n#{data}) if data.empty? || !data.include?(expected) 26 | end 27 | 28 | # OBJECT RESPONSE 29 | 30 | # response is an object with a field that is validated by a pre-defined regex 31 | Then( 32 | "the response #{HAVE_ALTERNATION} {field_name} of type {word}" 33 | ) do |field, type| 34 | regex = case type 35 | when 'datetime' then /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:[+|-]\d{2}:\d{2})?$/i 36 | when 'guid' then /^[{(]?[0-9A-F]{8}[-]?([0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$/i 37 | end 38 | type = 'string' if regex.nil? 39 | value = field.get_value(@response, type) 40 | field.validate_value(@response, value.to_s, Regexp.new(regex)) 41 | end 42 | 43 | # response is an object with a field that is validated by a custom regex 44 | Then("the response #{HAVE_ALTERNATION} {field_name} of type {word} that matches {string}") do |field, type, regex| 45 | value = field.get_value(@response, type) 46 | field.validate_value(@response, value.to_s, Regexp.new(regex)) 47 | end 48 | 49 | # response is an object with specific attributes having defined values 50 | Then("the response #{HAVE_ALTERNATION} (the )(following )attributes:") do |attributes| 51 | expected = parse_attributes(attributes.hashes) 52 | data = @response.get root_data_key 53 | raise %(Response did not match:\n#{expected.inspect}\n#{data}) if data.empty? || !data.deep_include?(expected) 54 | end 55 | 56 | # ARRAY RESPONSE 57 | 58 | # response is an array of objects 59 | Then('the response is a list of/containing {list_has_count} {field_name}') do |list_comparison, _item| 60 | list = @response.get_as_type root_data_key, 'array' 61 | unless list_comparison.compare(list.count) 62 | raise %(Expected #{list_comparison.to_string} items in array for path '#{root_data_key}', found: #{list.count} 63 | #{@response.to_json_s}) 64 | end 65 | end 66 | 67 | # response is an array of objects where the specified number of entries match the defined data attributes 68 | Then( 69 | '(the response is a list with/of/containing ){list_has_count} {word} '\ 70 | "#{HAVE_ALTERNATION} (the )(following )(data )attributes:" 71 | ) do |list_comparison, _item_name, attributes| 72 | expected = parse_attributes(attributes.hashes) 73 | data = @response.get_as_type root_data_key, 'array' 74 | matched = data.select { |item| !item.empty? && item.deep_include?(expected) } 75 | unless list_comparison.compare(matched.count) 76 | raise %(Expected #{list_comparison.to_string} items in array that matched:\n#{expected.inspect}\n#{data}) 77 | end 78 | end 79 | 80 | # response is an array of objects where the specified number of entries match the defined data attributes 81 | Then( 82 | '(the response is a list with/of/containing ){list_has_count} {item_name} {list_nesting}' 83 | ) do |list_comparison, _item_name, nesting| 84 | nesting.push( 85 | root: true, 86 | type: 'multiple', 87 | comparison: list_comparison 88 | ) 89 | data = @response.get get_key(nesting.grouping) 90 | if data.empty? || !nest_match_attributes(data, nesting.grouping, {}, false) 91 | raise %(Could not find a match for: #{nesting.match}\n#{@response.to_json_s}) 92 | end 93 | end 94 | 95 | # response is an array of objects where the specified number of entries match the defined data attributes 96 | Then( 97 | '(the response is a list with/of/containing ){list_has_count} {item_name} {list_nesting} '\ 98 | "#{HAVE_ALTERNATION} (the )(following )(data )attributes:" 99 | ) do |list_comparison, _item_name, nesting, attributes| 100 | expected = parse_attributes(attributes.hashes) 101 | nesting.push( 102 | root: true, 103 | type: 'multiple', 104 | comparison: list_comparison 105 | ) 106 | data = @response.get get_key(nesting.grouping) 107 | if data.empty? || !nest_match_attributes(data, nesting.grouping, expected, false) 108 | raise %(Could not find a match for: #{nesting.match}\n#{expected.inspect}\n#{@response.to_json_s}) 109 | end 110 | end 111 | 112 | # response is an array of objects where the specified number of entries match the defined string value 113 | Then( 114 | '(the response is a list with/of/containing ){list_has_count} {item_name} '\ 115 | 'having/containing/with (the )(following )value {string}' 116 | ) do |list_comparison, _item_name, value| 117 | expected = value 118 | data = @response.get_as_type root_data_key, 'array' 119 | matched = data.select { |item| !item.empty? && item.include?(expected) } 120 | unless list_comparison.compare(matched.count) 121 | raise %(Expected #{list_comparison.to_string} items in array that matched:\n#{expected}\n#{data}) 122 | end 123 | end 124 | 125 | # response is an array of objects where the specified number of entries match the defined string value 126 | Then( 127 | '(the response is a list with/of/containing ){list_has_count} {item_name} {list_nesting} '\ 128 | "#{HAVE_ALTERNATION} (the )(following )value {string}" 129 | ) do |list_comparison, _item_name, nesting, value| 130 | expected = value 131 | nesting.push( 132 | root: true, 133 | type: 'multiple', 134 | comparison: list_comparison 135 | ) 136 | data = @response.get get_key(nesting.grouping) 137 | if data.empty? || !nest_match_attributes(data, nesting.grouping, expected, true) 138 | raise %(Could not find a match for: #{nesting.match}\n#{expected}\n#{@response.to_json_s}) 139 | end 140 | end 141 | 142 | # HIERARCHICAL RESPONSE 143 | 144 | # response has the specified hierarchy of objects / lists where the 145 | # specified number of leaf items match the defined data attributes 146 | Then("the response {list_nesting} #{HAVE_ALTERNATION} (the )(following )attributes:") do |nesting, attributes| 147 | expected = parse_attributes(attributes.hashes) 148 | nesting.push( 149 | root: true, 150 | type: 'single' 151 | ) 152 | data = @response.get get_key(nesting.grouping) 153 | if data.empty? || !nest_match_attributes(data, nesting.grouping, expected, false) 154 | raise %( 155 | Could not find a match for: #{nesting.match} 156 | with: #{expected.inspect} 157 | in: #{data} 158 | #{@response.to_json_s} 159 | ) 160 | end 161 | end 162 | 163 | # response has the specified hierarchy of objects / lists where the 164 | # specified number of leaf items match the defined string value 165 | Then("the response {list_nesting} #{HAVE_ALTERNATION} (the )(following )value {string}") do |nesting, value| 166 | expected = value 167 | nesting.push( 168 | root: true, 169 | type: 'single' 170 | ) 171 | data = @response.get get_key(nesting.grouping) 172 | if data.empty? || !nest_match_attributes(data, nesting.grouping, expected, true) 173 | raise %(Could not find a match for: #{nesting.match}\n#{expected}\n#{@response.to_json_s}) 174 | end 175 | end 176 | 177 | # response has the specified hierarchy of objects / lists where the 178 | # specified number of leaf items is as expected only (no data checked) 179 | Then('the response {list_nesting}') do |nesting| 180 | nesting.push( 181 | root: true, 182 | type: 'single' 183 | ) 184 | data = @response.get get_key(nesting.grouping) 185 | if data.empty? || !nest_match_attributes(data, nesting.grouping, {}, false) 186 | raise %(Could not find a match for: #{nesting.match}\n#{@response.to_json_s}) 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /lib/cucumber-rest-bdd/steps/status.rb: -------------------------------------------------------------------------------- 1 | require 'cucumber-api/response' 2 | require 'cucumber-api/steps' 3 | 4 | ParameterType( 5 | name: 'item_type', 6 | regexp: /([\w\s]+)/, 7 | transformer: ->(s) { s }, 8 | use_for_snippets: false 9 | ) 10 | 11 | Then('the request is/was successful') do 12 | if @response.code < 200 || @response.code >= 300 13 | raise %(Expected Successful response code 2xx but was #{@response.code}) 14 | end 15 | end 16 | 17 | Then('the request is/was redirected') do 18 | if @response.code < 300 || @response.code >= 400 19 | raise %(Expected redirected response code 3xx but was #{@response.code}) 20 | end 21 | end 22 | 23 | Then('the request fail(s/ed)') do 24 | if @response.code < 400 || @response.code >= 600 25 | raise %(Expected failed response code 4xx\/5xx but was #{@response.code}) 26 | end 27 | end 28 | 29 | Then('the request is/was successful and a/the resource is/was created') do 30 | steps %(Then the response status should be "201") 31 | end 32 | 33 | Then('(the request is/was successful and )a/the {item_type} is/was created') do |_item_type| 34 | steps %(Then the response status should be "201") 35 | end 36 | 37 | Then('the request is/was successfully accepted') do 38 | steps %(Then the response status should be "202") 39 | end 40 | 41 | Then('the request is/was successful and (an )empty/blank/no response body is/was returned') do 42 | steps %(Then the response status should be "204") 43 | raise %(Expected the request body to be empty) unless @response.body.empty? 44 | end 45 | 46 | Then('the request fail(s/ed) because (the )it/resource is/was invalid') do 47 | steps %(Then the response status should be "400") 48 | end 49 | 50 | Then('the request fail(s/ed) because (the ){item_type} is/was invalid') do |_item_type| 51 | steps %(Then the response status should be "400") 52 | end 53 | 54 | Then('the request fail(s/ed) because (the )it/resource is/was/am/are unauthorised/unauthorized') do 55 | steps %(Then the response status should be "401") 56 | end 57 | 58 | Then('the request fail(s/ed) because (the ){item_type} is/was/am/are unauthorised/unauthorized') do |_item_type| 59 | steps %(Then the response status should be "401") 60 | end 61 | 62 | Then('the request fail(s/ed) because (the )it/resource is/was forbidden') do 63 | steps %(Then the response status should be "403") 64 | end 65 | 66 | Then('the request fail(s/ed) because (the ){item_type} is/was forbidden') do |_item_type| 67 | steps %(Then the response status should be "403") 68 | end 69 | 70 | Then('the request fail(s/ed) because (the )it/resource is/was not found') do 71 | steps %(Then the response status should be "404") 72 | end 73 | 74 | Then('the request fail(s/ed) because (the ){item_type} is/was not found') do |_item_type| 75 | steps %(Then the response status should be "404") 76 | end 77 | 78 | Then('the request fail(s/ed) because (the )it/resource is/was not allowed') do 79 | steps %(Then the response status should be "405") 80 | end 81 | 82 | Then('the request fail(s/ed) because (the ){item_type} is/was not allowed') do |_item_type| 83 | steps %(Then the response status should be "405") 84 | end 85 | 86 | Then('the request fail(s/ed) because there is/was/has a conflict') do 87 | steps %(Then the response status should be "409") 88 | end 89 | 90 | Then('the request fail(s/ed) because there is/was/has a conflict with {item_type}') do |_item_type| 91 | steps %(Then the response status should be "409") 92 | end 93 | 94 | Then('the request fail(s/ed) because (the )it/resource is/was/has gone') do 95 | steps %(Then the response status should be "410") 96 | end 97 | 98 | Then('the request fail(s/ed) because (the ){item_type} is/was/has gone') do |_item_type| 99 | steps %(Then the response status should be "410") 100 | end 101 | 102 | Then('the request fail(s/ed) because (the )it/resource is/was not implemented') do 103 | steps %(Then the response status should be "501") 104 | end 105 | 106 | Then('the request fail(s/ed) because (the ){item_type} is/was not implemented') do |_item_type| 107 | steps %(Then the response status should be "501") 108 | end 109 | -------------------------------------------------------------------------------- /lib/cucumber-rest-bdd/types.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/inflector' 2 | 3 | RESOURCE_NAME_SYNONYM = '\w+\b(?:\s+\w+\b)*?|`[^`]*`'.freeze 4 | FIELD_NAME_SYNONYM = '\w+\b(?:(?:\s+:)?\s+\w+\b)*?|`[^`]*`'.freeze 5 | 6 | ParameterType( 7 | name: 'resource_name', 8 | regexp: /#{RESOURCE_NAME_SYNONYM}/, 9 | transformer: ->(s) { get_resource(s) }, 10 | use_for_snippets: false 11 | ) 12 | 13 | ParameterType( 14 | name: 'field_name', 15 | regexp: /#{FIELD_NAME_SYNONYM}/, 16 | transformer: ->(s) { ResponseField.new(s) }, 17 | use_for_snippets: false 18 | ) 19 | 20 | # Add Boolean module to handle types 21 | module Boolean; end 22 | # True 23 | class TrueClass; include Boolean; end 24 | # False 25 | class FalseClass; include Boolean; end 26 | 27 | # Add Enum module to handle types 28 | module Enum; end 29 | # Enum is a type of string 30 | class String; include Enum; end 31 | 32 | # Handle parsing a field from a response 33 | class ResponseField 34 | def initialize(names) 35 | @fields = split_fields(names) 36 | end 37 | 38 | def to_json_path 39 | "#{root_data_key}#{@fields.join('.')}" 40 | end 41 | 42 | def get_value(response, type) 43 | response.get_as_type to_json_path, parse_type(type) 44 | end 45 | 46 | def validate_value(response, value, regex) 47 | raise "Expected #{json_path} value '#{value}' to match regex: #{regex}\n#{response.to_json_s}" \ 48 | if (regex =~ value).nil? 49 | end 50 | end 51 | 52 | def parse_type(type) 53 | replacements = { 54 | /^numeric$/i => 'numeric', 55 | /^int$/i => 'numeric', 56 | /^long$/i => 'numeric', 57 | /^number$/i => 'numeric', 58 | /^decimal$/i => 'numeric', 59 | /^double$/i => 'numeric', 60 | /^bool$/i => 'boolean', 61 | /^null$/i => 'nil_class', 62 | /^nil$/i => 'nil_class', 63 | /^string$/i => 'string', 64 | /^text$/i => 'string' 65 | } 66 | type.tr(' ', '_') 67 | replacements.each { |k, v| type.gsub!(k, v) } 68 | type 69 | end 70 | 71 | def string_to_type(value, type) 72 | replacements = { 73 | /^numeric$/i => 'integer', 74 | /^int$/i => 'integer', 75 | /^long$/i => 'integer', 76 | /^number$/i => 'integer', 77 | /^decimal$/i => 'float', 78 | /^double$/i => 'float', 79 | /^bool$/i => 'boolean', 80 | /^null$/i => 'nil_class', 81 | /^nil$/i => 'nil_class', 82 | /^string$/i => 'string', 83 | /^text$/i => 'string' 84 | } 85 | type.tr(' ', '_') 86 | replacements.each { |k, v| type.gsub!(k, v) } 87 | type = type.camelize.constantize 88 | # cannot use 'case type' which checks for instances of a type rather than type equality 89 | if type == Boolean then !(value =~ /true|yes/i).nil? 90 | elsif type == Enum then value.upcase.tr(' ', '_') 91 | elsif type == Float then value.to_f 92 | elsif type == Integer then value.to_i 93 | elsif type == NilClass then nil 94 | else value 95 | end 96 | end 97 | 98 | def get_resource(name) 99 | if name[0] == '`' && name[-1] == '`' 100 | name = name[1..-2] 101 | else 102 | name = name.parameterize 103 | name = ENV.key?('resource_single') && ENV['resource_single'] == 'true' ? name.singularize : name.pluralize 104 | end 105 | name 106 | end 107 | 108 | def root_data_key 109 | ENV.key?('data_key') && !ENV['data_key'].empty? ? "$.#{ENV['data_key']}." : '$.' 110 | end 111 | 112 | def json_path(names) 113 | "#{root_data_key}#{split_fields(names).join('.')}" 114 | end 115 | 116 | def split_fields(names) 117 | names.split(':').map { |n| parse_field(n.strip) } 118 | end 119 | 120 | def parse_field(name) 121 | if name[0] == '`' && name[-1] == '`' 122 | name = name[1..-2] 123 | elsif name[0] != '[' || name[-1] != ']' 124 | separator = ENV.key?('field_separator') ? ENV['field_separator'] : '_' 125 | name = name.parameterize(separator: separator) 126 | name = name.camelize(:lower) if ENV.key?('field_camel') && ENV['field_camel'] == 'true' 127 | end 128 | name 129 | end 130 | 131 | def parse_list_field(name) 132 | if name[0] == '`' && name[-1] == '`' 133 | name = name[1..-2] 134 | elsif name[0] != '[' || name[-1] != ']' 135 | separator = ENV.key?('field_separator') ? ENV['field_separator'] : '_' 136 | name = name.parameterize(separator: separator) 137 | name = name.pluralize 138 | name = name.camelize(:lower) if ENV.key?('field_camel') && ENV['field_camel'] == 'true' 139 | end 140 | name 141 | end 142 | 143 | def parse_attributes(hashes) 144 | hashes.each_with_object({}) do |row, hash| 145 | name = row['attribute'] 146 | value = row['value'] 147 | type = row['type'] 148 | value = resolve_functions(value) 149 | value = resolve(value) 150 | value.gsub!(/\\n/, "\n") 151 | names = split_fields(name) 152 | new_hash = names.reverse.inject(string_to_type(value, type)) { |a, n| add_to_hash(a, n) } 153 | hash.deep_merge!(new_hash) { |_, old, new| new.is_a?(Array) ? merge_arrays(old, new) : new } 154 | end 155 | end 156 | 157 | def resolve_functions(value) 158 | value.gsub!(/\[([a-zA-Z0-9_]+)\]/) do |s| 159 | s.gsub!(/[\[\]]/, '') 160 | case s.downcase 161 | when 'datetime' 162 | Time.now.strftime('%Y%m%d%H%M%S') 163 | else 164 | raise 'Unrecognised function ' + s + '?' 165 | end 166 | end 167 | value 168 | end 169 | 170 | def add_to_hash(hash, node) 171 | result = nil 172 | if node[0] == '[' && node[-1] == ']' 173 | array = Array.new(node[1..-2].to_i + 1) 174 | array[node[1..-2].to_i] = hash 175 | result = array 176 | end 177 | !result.nil? ? result : { node => hash } 178 | end 179 | 180 | def merge_arrays(first, second) 181 | new_length = [first.length, second.length].max 182 | new_array = Array.new(new_length) 183 | new_length.times do |n| 184 | new_array[n] = if second[n].nil? 185 | first[n] 186 | else 187 | new_array[n] = if first[n].nil? 188 | second[n] 189 | else 190 | first[n].deep_merge!(second[n]) do |_, old, new| 191 | new.is_a?(Array) ? merge_arrays(old, new) : new 192 | end 193 | end 194 | end 195 | end 196 | new_array 197 | end 198 | -------------------------------------------------------------------------------- /lib/cucumber-rest-bdd/url.rb: -------------------------------------------------------------------------------- 1 | def get_url(path) 2 | raise %(Please set an 'endpoint' environment variable provided with the url of the api) unless ENV.key?('endpoint') 3 | 4 | url = ENV['endpoint'] 5 | url = "#{url}/" unless url.end_with?('/') 6 | url = "#{url}#{@urlbasepath}/" unless @urlbasepath.to_s.empty? 7 | url = "#{url}#{path}" unless path.empty? 8 | url 9 | end 10 | -------------------------------------------------------------------------------- /rubocop/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.5-alpine 2 | 3 | RUN set +xe \ 4 | && apk add -u --no-cache \ 5 | bash \ 6 | su-exec \ 7 | ca-certificates \ 8 | tini 9 | 10 | RUN set +xe \ 11 | && apk add --no-cache --virtual .ruby-builddeps \ 12 | autoconf \ 13 | bison \ 14 | bzip2 \ 15 | bzip2-dev \ 16 | coreutils \ 17 | dpkg \ 18 | dpkg-dev \ 19 | gcc \ 20 | gdbm-dev \ 21 | glib-dev \ 22 | libc-dev \ 23 | libffi-dev \ 24 | libressl \ 25 | libressl-dev \ 26 | libxml2-dev \ 27 | libxslt-dev \ 28 | linux-headers \ 29 | make \ 30 | ncurses-dev \ 31 | procps \ 32 | readline-dev \ 33 | ruby \ 34 | tar \ 35 | xz \ 36 | yaml-dev \ 37 | zlib-dev \ 38 | && gem install rubocop \ 39 | && apk del .ruby-builddeps 40 | 41 | ENTRYPOINT ["rubocop"] 42 | -------------------------------------------------------------------------------- /server/error.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res, next) => { 2 | if (req.path == '/errors/list') { 3 | res.status(404) 4 | res.locals.data = { 5 | errors: [{ message: 'Not Found' }] 6 | } 7 | } else if (req.path == '/errors/single') { 8 | res.status(400) 9 | res.locals.data = { 10 | error: { message: 'Bad Request' } 11 | } 12 | } 13 | next() 14 | } 15 | -------------------------------------------------------------------------------- /server/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/:r1/:id1/:r2/:id2": "/:r2/:id2", 3 | "/users/:uid/albums/:alid/photos": "/photos?albumId=:alid", 4 | "/users/:uid/albums/:alid/photos/:pid": "/photos/:pid" 5 | } --------------------------------------------------------------------------------