├── .gitignore ├── .rspec ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── client-api.gemspec ├── data ├── request │ ├── post.json │ └── upload.png ├── response │ └── post.json └── schema │ └── get_user_schema.json ├── lib ├── client-api.rb └── client-api │ ├── base.rb │ ├── loggers.rb │ ├── request.rb │ ├── settings.rb │ ├── validator.rb │ └── version.rb └── spec ├── client ├── basic_auth_spec.rb ├── custom_header_spec.rb ├── default_validation_spec.rb ├── empty_validation_spec.rb ├── header_validation_spec.rb ├── json_body_template.rb ├── json_schema_validation_spec.rb ├── json_value_validation_spec.rb ├── response_status_spec.rb ├── scheme_spec.rb ├── size_validation_spec.rb └── sorting_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | 13 | Gemfile.lock 14 | .idea/ 15 | .DS_Store 16 | .byebug_history 17 | output/ 18 | logs/ 19 | bin/ 20 | vendor/ 21 | *.gem 22 | **/demo_validation_spec.rb 23 | **/custom_url_spec.rb 24 | **/demo_spec.rb 25 | /data/response/post_new.json -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: false 3 | language: ruby 4 | cache: bundler 5 | rvm: 6 | - 2.6.3 7 | before_install: gem install bundler -v 2.0.2 8 | script: bundle exec rake spec -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at sams.prashanth@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 prashanth-sams 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ClientApi 2 | 3 | [![Gem Version](https://badge.fury.io/rb/client-api.svg)](http://badge.fury.io/rb/client-api) 4 | [![Build Status](https://travis-ci.org/prashanth-sams/client-api.svg?branch=master)](https://travis-ci.org/prashanth-sams/client-api) 5 | > HTTP REST API client for testing application APIs based on the ruby’s RSpec framework that binds a complete api automation framework setup within itself 6 | 7 | ### Features 8 | - [x] Custom Header, URL, and Timeout support 9 | - [x] URL query string customization 10 | - [x] Datatype and key-pair value validation 11 | - [x] Single key-pair response validation 12 | - [x] Multi key-pair response validation 13 | - [x] JSON response schema validation 14 | - [x] JSON response content validation 15 | - [x] JSON response size validation 16 | - [x] JSON response is empty? validation 17 | - [x] JSON response has specific key? validation 18 | - [x] JSON response array-list sorting validation (descending, ascending) 19 | - [x] Response headers validation 20 | - [x] JSON template as body and schema 21 | - [x] Support to store JSON responses of each tests for the current run 22 | - [x] Logs support for debug 23 | - [x] Custom logs remover 24 | - [x] Auto-handle SSL for http(s) schemes 25 | 26 | ## Installation 27 | 28 | Add this line to your application's Gemfile: 29 | 30 | ```ruby 31 | gem 'client-api' 32 | ``` 33 | 34 | And then execute: 35 | ```bash 36 | $ bundle 37 | ``` 38 | 39 | Or install it yourself as: 40 | ```bash 41 | $ gem install client-api 42 | ``` 43 | 44 | Import the library in your env file 45 | ```ruby 46 | require 'client-api' 47 | ``` 48 | 49 | ## #Usage outline 50 | 51 | Add this config snippet in the `spec_helper.rb` file: 52 | ```ruby 53 | ClientApi.configure do |config| 54 | # all these configs are optional; comment out the config if not required 55 | config.base_url = 'https://reqres.in' 56 | config.headers = {'Content-Type' => 'application/json', 'Accept' => 'application/json'} 57 | config.basic_auth = {'Username' => 'ahamilton@apigee.com', 'Password' => 'myp@ssw0rd'} 58 | config.json_output = {'Dirname' => './output', 'Filename' => 'test'} 59 | config.time_out = 10 # in secs 60 | config.logger = {'Dirname' => './logs', 'Filename' => 'test', 'StoreFilesCount' => 2} 61 | 62 | # add this snippet only if the logger is enabled 63 | config.before(:each) do |scenario| 64 | ClientApi::Request.new(scenario) 65 | end 66 | end 67 | ``` 68 | Create `client-api` object with custom variable 69 | ```ruby 70 | api = ClientApi::Api.new 71 | ``` 72 | 73 | RSpec test scenarios look like, 74 | ```ruby 75 | it "GET request" do 76 | api = ClientApi::Api.new 77 | 78 | api.get('/api/users') 79 | expect(api.status).to eq(200) 80 | expect(api.code).to eq(200) 81 | expect(api.message).to eq('OK') 82 | end 83 | 84 | it "POST request" do 85 | api.post('/api/users', {"name": "prashanth sams"}) 86 | expect(api.status).to eq(201) 87 | end 88 | 89 | it "DELETE request" do 90 | api.delete('/api/users/3') 91 | expect(api.status).to eq(204) 92 | end 93 | 94 | it "PUT request" do 95 | api.put('/api/users/2', {"data":{"email":"prashanth@mail.com","first_name":"Prashanth","last_name":"Sams"}}) 96 | expect(api.status).to eq(200) 97 | end 98 | 99 | it "PATCH request" do 100 | api.patch('/api/users/2', {"data":{"email":"prashanth@mail.com","first_name":"Prashanth","last_name":"Sams"}}) 101 | expect(api.status).to eq(200) 102 | end 103 | 104 | # For exceptional cases with body in the GET/DELETE request 105 | it "GET request with JSON body" do 106 | api.get_with_body('/api/users', { "count": 2 }) 107 | expect(api.status).to eq(200) 108 | end 109 | 110 | it "DELETE request with JSON body" do 111 | api.delete_with_body('/api/users', { "count": 2 }) 112 | expect(api.status).to eq(200) 113 | end 114 | 115 | # Customize URL query string as a filter 116 | it "Custom URL query string" do 117 | api.get( 118 | { 119 | :url => '/location?', 120 | :query => { 121 | 'sort': 'name', 122 | 'fields[count]': '50', 123 | 'fields[path_prefix]': '6', 124 | 'filter[name]': 'Los Angels' 125 | } 126 | } 127 | ) 128 | end 129 | 130 | # For POST request with multi-form as body 131 | it "POST request with multi-form as body" do 132 | api.post('/api/upload', 133 | payload( 134 | 'type' => 'multipart/form-data', 135 | 'data' => { 136 | 'file': './data/request/upload.png' 137 | } 138 | ) 139 | ) 140 | 141 | expect(api.code).to eq(200) 142 | end 143 | ``` 144 | 145 | ## Validation shortcuts 146 | 147 | #### Default validation 148 | 149 | > key features 150 | - datatype validation 151 | - key-pair value validation 152 | - value size validation 153 | - is value empty validation 154 | - key exist or key not-exist validation 155 | - single key-pair validation 156 | - multi key-pair validation 157 | 158 | > what to know? 159 | - operator field is optional when `"operator": "=="` 160 | - exception is handled for the invalid key (say, `key: 'post->0->name'`), if the `has_key` field is not added in the validation 161 | 162 | 163 | 164 | 167 | 170 | 173 | 176 | 179 | 182 | 183 | 184 | 197 | 208 | 219 | 230 | 246 | 262 | 263 |
165 | General Syntax 166 | 168 | Syntax | Model 2 169 | 171 | Syntax | Model 3 172 | 174 | Syntax | Model 4 175 | 177 | Syntax | Model 5 178 | 180 | Syntax | Model 6 181 |
185 |
186 | validate(
187 |     api.body,
188 |     {
189 |         key: '',
190 |         operator: '', 
191 |         value: '', 
192 |         type: ''
193 |     }
194 | )
195 |             
196 |
198 |
199 | validate(
200 |     api.body,
201 |     {
202 |         key: '', 
203 |         size: 0
204 |     }
205 | )
206 |             
207 |
209 |
210 | validate(
211 |     api.body,
212 |     {
213 |         key: '', 
214 |         empty: true
215 |     }
216 | )
217 |             
218 |
220 |
221 | validate(
222 |     api.body,
223 |     {
224 |         key: '', 
225 |         has_key: true
226 |     }
227 | )
228 |             
229 |
231 |
232 | validate(
233 |     api.body,
234 |     {
235 |         key: '',
236 |         operator: '', 
237 |         value: '', 
238 |         type: '', 
239 |         size: 2,
240 |         empty: true,
241 |         has_key: false
242 |     }
243 | )
244 |             
245 |
247 |
248 | validate(
249 |     api.body,
250 |     {
251 |         key: '', 
252 |         type: ''
253 |     },
254 |     {
255 |         key: '', 
256 |         operator: '', 
257 |         value: ''
258 |     }
259 | )
260 |             
261 |
264 | 265 | #### JSON response content validation 266 | 267 | > key benefits 268 | - the most recommended validation for fixed / static JSON responses 269 | - validates each JSON content value 270 | 271 | > what to know? 272 | - replace `null` with `nil` in the expected json (whenever applicable); cos, ruby don't know what is `null` 273 | 274 | 275 | 276 | 277 | 280 | 283 | 284 | 285 | 307 | 323 | 324 |
278 | General Syntax 279 | 281 | Syntax | Model 2 282 |
286 |
287 | validate_json(
288 |     {
289 |         "data":
290 |             {
291 |                 "id": 2,
292 |                 "first_name": "Prashanth",
293 |                 "last_name": "Sams",
294 |             }
295 |     },
296 |     {
297 |         "data":
298 |             {
299 |                 "id": 2,
300 |                 "first_name": "Prashanth",
301 |                 "last_name": "Sams",
302 |             }
303 |     }
304 | )
305 |             
306 |
308 |
309 | validate_json(
310 |     api.body,
311 |     {
312 |         "data":
313 |             {
314 |                 "id": 2,
315 |                 "first_name": "Prashanth",
316 |                 "last_name": "Sams",
317 |                 "link": nil
318 |             }
319 |     }
320 | )
321 |             
322 |
325 | 326 | #### JSON response sorting validation 327 | 328 | > key benefits 329 | - validates an array of response key-pair values with ascending or descending soring algorithm. For more details, check `sort_spec.rb` 330 | 331 | 332 | 333 | 336 | 339 | 340 | 341 | 353 | 365 | 366 |
334 | General Syntax 335 | 337 | Syntax | Model 2 338 |
342 |
343 | validate_list(
344 |   api.body,
345 |   {
346 |       "key": "posts",
347 |       "unit": "id",
348 |       "sort": "ascending"
349 |   }
350 | )
351 |             
352 |
354 |
355 | validate_list(
356 |   api.body,
357 |   {
358 |       "key": "posts",
359 |       "unit": "id",
360 |       "sort": "descending"
361 |   }
362 | )
363 |             
364 |
367 | 368 | #### JSON response headers validation 369 | 370 | > key benefits 371 | - validates any response headers 372 | 373 | 374 | 375 | 378 | 381 | 382 | 383 | 395 | 411 | 412 |
376 | General Syntax 377 | 379 | Syntax | Model 2 380 |
384 |
385 | validate_headers(
386 |     api.response_headers,
387 |     {
388 |        key: '',
389 |        operator: '',
390 |        value: ''
391 |     }
392 | )
393 |             
394 |
396 |
397 | validate_headers(
398 |     api.response_headers,
399 |     {
400 |        key: "connection",
401 |        operator: "!=",
402 |        value: "open"
403 |     },{
404 |        key: "vary",
405 |        operator: "==",
406 |        value: "Origin, Accept-Encoding"
407 |     }
408 | )
409 |             
410 |
413 | 414 | ## #General usage 415 | 416 | Using `json` template as body 417 | ```ruby 418 | it "JSON template as body" do 419 | api.post('/api/users', payload("./data/request/post.json")) 420 | expect(api.status).to eq(201) 421 | end 422 | ``` 423 | Add custom header 424 | ```ruby 425 | it "GET request with custom header" do 426 | api.get('/api/users', {'Content-Type' => 'application/json', 'Accept' => 'application/json'}) 427 | expect(api.status).to eq(200) 428 | end 429 | 430 | it "PATCH request with custom header" do 431 | api.patch('/api/users/2', {"data":{"email":"prashanth@mail.com","first_name":"Prashanth","last_name":"Sams"}}, {'Content-Type' => 'application/json', 'Accept' => 'application/json'}) 432 | expect(api.status).to eq(200) 433 | end 434 | ``` 435 | Full url support 436 | ```ruby 437 | it "full url", :post do 438 | api.post('https://api.enterprise.apigee.com/v1/organizations/ahamilton-eval',{},{'Authorization' => 'Basic YWhhbWlsdG9uQGFwaWdlZS5jb206bXlwYXNzdzByZAo'}) 439 | expect(api.status).to eq(403) 440 | end 441 | ``` 442 | Basic Authentication 443 | ```ruby 444 | ClientApi.configure do |config| 445 | ... 446 | config.basic_auth = {'Username' => 'ahamilton@apigee.com', 'Password' => 'myp@ssw0rd'} 447 | end 448 | ``` 449 | Custom Timeout in secs 450 | ```ruby 451 | ClientApi.configure do |config| 452 | ... 453 | config.time_out = 10 # in secs 454 | end 455 | ``` 456 | Output as `json` template 457 | ```ruby 458 | ClientApi.configure do |config| 459 | ... 460 | config.json_output = {'Dirname' => './output', 'Filename' => 'sample'} 461 | end 462 | ``` 463 | 464 | 465 | ## Logs 466 | > Logs are optional in this library; you can do so through config in `spec_helper.rb`. The param,`StoreFilesCount` will keep the custom files as logs; you can remove it, if not needed. 467 | 468 | ```ruby 469 | ClientApi.configure do |config| 470 | ... 471 | config.logger = {'Dirname' => './logs', 'Filename' => 'test', 'StoreFilesCount' => 5} 472 | 473 | config.before(:each) do |scenario| 474 | ClientApi::Request.new(scenario) 475 | end 476 | end 477 | ``` 478 | 479 | 480 | 481 | ## #Validation | more info. 482 | 483 | > Single key-pair value JSON response validator 484 | 485 | Validates JSON response `value`, `datatype`, `size`, `is value empty?`, and `key exist?` 486 | ```ruby 487 | validate( 488 | api.body, 489 | { 490 | "key": "name", 491 | "value": "prashanth sams", 492 | "operator": "==", 493 | "type": 'string' 494 | } 495 | ) 496 | ``` 497 | > Multi key-pair values response validator 498 | 499 | Validates more than one key-pair values 500 | ```ruby 501 | validate( 502 | api.body, 503 | { 504 | "key": "name", 505 | "value": "prashanth sams", 506 | "type": 'string' 507 | }, 508 | { 509 | "key": "event", 510 | "operator": "eql?", 511 | "type": 'boolean' 512 | }, 513 | { 514 | "key": "posts->1->enabled", 515 | "value": false, 516 | "operator": "!=", 517 | "type": 'boolean' 518 | }, 519 | { 520 | "key": "profile->name->id", 521 | "value": 2, 522 | "operator": "==", 523 | "type": 'integer' 524 | }, 525 | { 526 | "key": "profile->name->id", 527 | "value": 2, 528 | "operator": "<", 529 | "type": 'integer' 530 | }, 531 | { 532 | "key": "profile->name->id", 533 | "operator": ">=", 534 | "value": 2, 535 | }, 536 | { 537 | "key": "post1->0->name", 538 | "operator": "contains", 539 | "value": "Sams" 540 | }, 541 | { 542 | "key": "post2->0->id", 543 | "operator": "include", 544 | "value": 34, 545 | "type": 'integer' 546 | }, 547 | { 548 | "key": "post1->0->available", 549 | "value": true, 550 | "operator": "not contains", 551 | "type": "boolean" 552 | } 553 | ) 554 | ``` 555 | > JSON response size validator 556 | 557 | Validates the total size of the JSON array 558 | ```ruby 559 | validate( 560 | api.body, 561 | { 562 | "key": "name", 563 | "size": 2 564 | }, 565 | { 566 | "key": "name", 567 | "operator": "==", 568 | "value": "Sams", 569 | "type": "string", 570 | "has_key": true, 571 | "empty": false, 572 | "size": 2 573 | } 574 | ) 575 | ``` 576 | > JSON response value empty? validator 577 | 578 | Validates if the key has empty value or not 579 | ```ruby 580 | validate( 581 | api.body, 582 | { 583 | "key": "0->name", 584 | "empty": false 585 | }, 586 | { 587 | "key": "name", 588 | "operator": "==", 589 | "value": "Sams", 590 | "type": "string", 591 | "size": 2, 592 | "has_key": true, 593 | "empty": false 594 | } 595 | ) 596 | ``` 597 | > JSON response has specific key? validator 598 | 599 | Validates if the key exist or not 600 | ```ruby 601 | validate( 602 | api.body, 603 | { 604 | "key": "0->name", 605 | "has_key": true 606 | }, 607 | { 608 | "key": "name", 609 | "operator": "==", 610 | "value": "", 611 | "type": "string", 612 | "size": 2, 613 | "empty": true, 614 | "has_key": true 615 | } 616 | ) 617 | ``` 618 | 619 | ###### Operator 620 | | Type | options | 621 | | --- | --- | 622 | | Equal | `=`, `==`, `eql`, `eql?`, `equal`, `equal?` | 623 | | Not Equal | `!`, `!=`, `!eql`, `!eql?`, `not eql`, `not equal`, `!equal?` | 624 | | Greater than | `>`, `>=`, `greater than`, `greater than or equal to` | 625 | | Less than | `<`, `<=`, `less than`, `less than or equal to`, `lesser than`, `lesser than or equal to` | 626 | | Contains | `contains`, `has`, `contains?`, `has?`, `include`, `include?` | 627 | | Not Contains | `not contains`, `!contains`, `not include`, `!include` | 628 | 629 | ###### Datatype 630 | | Type | options | 631 | | --- | --- | 632 | | String | `string`, `str` | 633 | | Integer | `integer`, `int` | 634 | | Symbol | `symbol`, `sym` | 635 | | Boolean | `boolean`, `bool` | 636 | | Array | `array`, `arr` | 637 | | Object | `object`, `obj` | 638 | | Float | `float` | 639 | | Hash | `hash` | 640 | | Complex | `complex` | 641 | | Rational | `rational` | 642 | | Fixnum | `fixnum` | 643 | | Falseclass | `falseclass`, `false` | 644 | | Trueclass | `trueclass`, `true` | 645 | | Bignum | `bignum` | 646 | 647 | #### JSON response schema validation 648 | ```ruby 649 | validate_schema( 650 | schema_from_json('./data/schema/get_user_schema.json'), 651 | { 652 | "data": 653 | { 654 | "id": 2, 655 | "email": "janet.weaver@reqres.in", 656 | "firstd_name": "Janet", 657 | "last_name": "Weaver", 658 | "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg" 659 | } 660 | } 661 | ) 662 | ``` 663 | ```ruby 664 | validate_schema( 665 | { 666 | "required": [ 667 | "data" 668 | ], 669 | "type": "object", 670 | "properties": { 671 | "data": { 672 | "type": "object", 673 | "required": [ 674 | "id", "email", "first_name", "last_name", "avatar" 675 | ], 676 | "properties": { 677 | "id": { 678 | "type": "integer" 679 | }, 680 | "email": { 681 | "type": "string" 682 | }, 683 | "first_name": { 684 | "type": "string" 685 | }, 686 | "last_name": { 687 | "type": "string" 688 | }, 689 | "avatar": { 690 | "type": "string" 691 | } 692 | } 693 | } 694 | } 695 | }, 696 | { 697 | "data": 698 | { 699 | "id": 2, 700 | "email": "janet.weaver@reqres.in", 701 | "first_name": "Janet", 702 | "last_name": "Weaver", 703 | "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg" 704 | } 705 | } 706 | ) 707 | ``` 708 | ```ruby 709 | validate_schema( 710 | schema_from_json('./data/schema/get_user_schema.json'), 711 | api.body 712 | ) 713 | ``` 714 | 715 | #### JSON response content validation 716 | > json response content value validation as a structure 717 | ```ruby 718 | actual_body = { 719 | "posts": 720 | { 721 | "prashanth": { 722 | "id": 1, 723 | "title": "Post 1" 724 | }, 725 | "sams": { 726 | "id": 2, 727 | "title": "Post 2" 728 | } 729 | }, 730 | "profile": 731 | { 732 | "id": 44, 733 | "title": "Post 44" 734 | } 735 | } 736 | 737 | validate_json( actual_body, 738 | { 739 | "posts": 740 | { 741 | "prashanth": { 742 | "id": 1, 743 | "title": "Post 1" 744 | }, 745 | "sams": { 746 | "id": 2 747 | } 748 | }, 749 | "profile": 750 | { 751 | "title": "Post 44" 752 | } 753 | }) 754 | ``` 755 | ```ruby 756 | validate_json( api.body, 757 | { 758 | "posts": [ 759 | { 760 | "id": 2, 761 | "title": "Post 2" 762 | } 763 | ], 764 | "profile": { 765 | "name": "typicode" 766 | } 767 | } 768 | ) 769 | ``` 770 | 771 | #### Response headers validation 772 | ```ruby 773 | validate_headers( 774 | api.response_headers, 775 | { 776 | key: "connection", 777 | operator: "!=", 778 | value: "open" 779 | }, 780 | { 781 | key: "vary", 782 | operator: "==", 783 | value: "Origin, Accept-Encoding" 784 | } 785 | ) 786 | ``` 787 | 788 | #### Is there a demo available for this gem? 789 | Yes, you can use this demo as an example, https://github.com/prashanth-sams/client-api 790 | ``` 791 | rake spec 792 | ``` -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "client/api" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /client-api.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("lib", __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require "client-api/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "client-api" 7 | spec.version = ClientApi::VERSION 8 | spec.authors = ["Prashanth Sams"] 9 | spec.email = ["sams.prashanth@gmail.com"] 10 | 11 | spec.summary = "HTTP REST API client for testing application APIs based on the ruby’s RSpec framework that binds a complete api automation framework setup within itself" 12 | spec.homepage = "https://github.com/prashanth-sams/client-api" 13 | spec.license = "MIT" 14 | 15 | spec.metadata["homepage_uri"] = spec.homepage 16 | spec.metadata["source_code_uri"] = "https://github.com/prashanth-sams/client-api" 17 | spec.metadata["documentation_uri"] = "https://www.rubydoc.info/github/prashanth-sams/client-api/master" 18 | spec.metadata["bug_tracker_uri"] = "https://github.com/prashanth-sams/client-api/issues" 19 | 20 | # Specify which files should be added to the gem when it is released. 21 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 22 | spec.files = ["lib/client-api.rb", "lib/client-api/base.rb", "lib/client-api/loggers.rb", "lib/client-api/request.rb", "lib/client-api/settings.rb", "lib/client-api/validator.rb", "lib/client-api/version.rb"] 23 | spec.bindir = "exe" 24 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 25 | spec.require_paths = ["lib"] 26 | 27 | spec.add_development_dependency "bundler" 28 | spec.add_development_dependency "rake" 29 | spec.add_development_dependency "rspec", "~> 3.0" 30 | spec.add_development_dependency "byebug", "~> 11.0" 31 | 32 | spec.add_runtime_dependency "json-schema", '~> 2.8' 33 | spec.add_runtime_dependency "logger", "~> 1.0" 34 | end 35 | -------------------------------------------------------------------------------- /data/request/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prashanth sams" 3 | } -------------------------------------------------------------------------------- /data/request/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prashanth-sams/client-api/9cbb6379f952566f1c8e005a121ef5c74192151d/data/request/upload.png -------------------------------------------------------------------------------- /data/response/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prashanth sams" 3 | } -------------------------------------------------------------------------------- /data/schema/get_user_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": [ 3 | "data" 4 | ], 5 | "type": "object", 6 | "properties": { 7 | "data": { 8 | "type": "object", 9 | "required": [ 10 | "id", 11 | "email", 12 | "first_name", 13 | "last_name", 14 | "avatar" 15 | ], 16 | "properties": { 17 | "id": { 18 | "type": "integer" 19 | }, 20 | "email": { 21 | "type": "string" 22 | }, 23 | "first_name": { 24 | "type": "string" 25 | }, 26 | "last_name": { 27 | "type": "string" 28 | }, 29 | "avatar": { 30 | "type": "string" 31 | } 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /lib/client-api.rb: -------------------------------------------------------------------------------- 1 | require "client-api/version" 2 | require "client-api/settings" 3 | require "client-api/base" 4 | require "client-api/request" 5 | require "client-api/validator" 6 | require "client-api/loggers" 7 | 8 | RSpec.configure do |config| 9 | config.add_setting :base_url 10 | config.add_setting :headers 11 | config.add_setting :basic_auth 12 | config.add_setting :json_output 13 | config.add_setting :time_out 14 | config.include ClientApi 15 | end 16 | 17 | include Loggers -------------------------------------------------------------------------------- /lib/client-api/base.rb: -------------------------------------------------------------------------------- 1 | require_relative 'request' 2 | 3 | module ClientApi 4 | 5 | class Api < ClientApi::Request 6 | 7 | include ClientApi 8 | 9 | def initialize 10 | ((FileUtils.rm Dir.glob("./#{json_output['Dirname']}/*.json"); $roo = true)) if json_output && $roo == nil 11 | end 12 | 13 | def get(url, headers = nil) 14 | @output = get_request(url_generator(url), :headers => headers) 15 | self.post_logger if $logger 16 | self.output_json_body if json_output 17 | end 18 | 19 | def get_with_body(url, body = nil, headers = nil) 20 | @output = get_with_body_request(url_generator(url), :body => body, :headers => headers) 21 | self.post_logger if $logger 22 | self.output_json_body if json_output 23 | end 24 | 25 | def post(url, body, headers = nil) 26 | if body.is_a? Hash 27 | if body['type'] && body['data'] 28 | @output = post_request_x(url_generator(url), :body => body, :headers => headers) 29 | else 30 | @output = post_request(url_generator(url), :body => body, :headers => headers) 31 | end 32 | else 33 | raise 'invalid body' 34 | end 35 | 36 | self.post_logger if $logger 37 | self.output_json_body if json_output 38 | end 39 | 40 | def delete(url, headers = nil) 41 | @output = delete_request(url_generator(url), :headers => headers) 42 | self.post_logger if $logger 43 | self.output_json_body if json_output 44 | end 45 | 46 | def delete_with_body(url, body = nil, headers = nil) 47 | @output = delete_with_body_request(url_generator(url), :body => body, :headers => headers) 48 | self.post_logger if $logger 49 | self.output_json_body if json_output 50 | end 51 | 52 | def put(url, body, headers = nil) 53 | @output = put_request(url_generator(url), :body => body, :headers => headers) 54 | self.post_logger if $logger 55 | self.output_json_body if json_output 56 | end 57 | 58 | def patch(url, body, headers = nil) 59 | @output = patch_request(url_generator(url), :body => body, :headers => headers) 60 | self.post_logger if $logger 61 | self.output_json_body if json_output 62 | end 63 | 64 | def status 65 | @output.code.to_i 66 | end 67 | 68 | def body 69 | unless ['', nil, '{}'].any? { |e| e == @output.body } || pdf_response_header 70 | JSON.parse(%{#{@output.body}}) 71 | end 72 | end 73 | 74 | def output_json_body 75 | unless ['', nil, '{}'].any? { |e| e == @output.body } || pdf_response_header 76 | unless json_output['Dirname'] == nil 77 | FileUtils.mkdir_p "#{json_output['Dirname']}" 78 | time_now = (Time.now.to_f).to_s.gsub('.','') 79 | begin 80 | File.open("./#{json_output['Dirname']}/#{json_output['Filename']+"_"+time_now}""#{time_now}"".json", "wb") {|file| file.puts JSON.pretty_generate(JSON.parse(@output.body))} 81 | rescue StandardError => e 82 | raise("\n"+" Not a compatible (or) Invalid JSON response => [kindly check the uri & request details]".brown + " \n\n #{e.message}") 83 | end 84 | end 85 | end 86 | end 87 | 88 | def response_headers 89 | resp_headers = {} 90 | @output.response.each { |key, value| resp_headers.merge!(key.to_s => value.to_s) } 91 | end 92 | 93 | def pdf_response_header 94 | response_headers.map do |data| 95 | if data[0].downcase == 'Content-Type'.downcase && (data[1][0].include? 'application/pdf') 96 | return true 97 | end 98 | end 99 | false 100 | end 101 | 102 | def message 103 | @output.message 104 | end 105 | 106 | def post_logger 107 | ((['', nil, '{}'].any? { |e| e == @output.body }) || pdf_response_header) ? res_body = 'empty response body' : res_body = body 108 | 109 | $logger.debug("Response code == #{@output.code.to_i}") 110 | $logger.debug("Response body == #{res_body}") 111 | 112 | log_headers = {} 113 | @output.response.each { |key, value| log_headers.merge!(key.to_s => value.to_s) } 114 | $logger.debug("Response headers == #{log_headers}") 115 | $logger.debug("=====================================================================================") 116 | end 117 | 118 | alias :code :status 119 | alias :resp :body 120 | end 121 | 122 | def payload(args) 123 | if args['type'].nil? 124 | JSON.parse(File.read(args)) 125 | else 126 | case args['type'].downcase 127 | when 'multipart/form-data', 'application/x-www-form-urlencoded' 128 | args 129 | else 130 | raise "invalid body type | try: payload('./data/request/file.png', 'multipart/form-data')" 131 | end 132 | end 133 | end 134 | 135 | def url_generator(url) 136 | begin 137 | if url.count == 2 138 | raise('":url"'.green + ' field is missing in url'.red) if url[:url].nil? 139 | raise('":query"'.green + ' field is missing in url'.red) if url[:query].nil? 140 | 141 | query = url[:url].include?('?') ? [url[:url]] : [url[:url].concat('?')] 142 | 143 | url[:query].map do |val| 144 | query << val[0].to_s + "=" + val[1].gsub(' ','%20') + "&" 145 | end 146 | return query.join.delete_suffix('&') 147 | end 148 | rescue ArgumentError 149 | url 150 | end 151 | end 152 | 153 | alias :schema_from_json :payload 154 | 155 | end -------------------------------------------------------------------------------- /lib/client-api/loggers.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module Loggers 4 | 5 | def logger=(logs) 6 | output_logs_dir = logs['Dirname'] 7 | output_logs_filename = logs['Filename'] 8 | 9 | now = (Time.now.to_f * 1000).to_i 10 | $logger = Logger.new(STDOUT) 11 | $logger.datetime_format = '%Y-%m-%d %H:%M:%S' 12 | Dir.mkdir("./#{output_logs_dir}") unless File.exist?("./#{output_logs_dir}") 13 | 14 | if logs['StoreFilesCount'] && (logs['StoreFilesCount'] != nil || logs['StoreFilesCount'] != {} || logs['StoreFilesCount'] != empty) 15 | file_count = Dir["./#{output_logs_dir}/*.log"].length 16 | Dir["./#{output_logs_dir}/*.log"].sort_by {|f| File.ctime(f)}.reverse.last(file_count - "#{logs['StoreFilesCount']}".to_i).map {|junk_file| File.delete(junk_file)} if file_count > logs['StoreFilesCount'] 17 | end 18 | 19 | $logger = Logger.new(File.new("#{output_logs_dir}/#{output_logs_filename}_#{now}.log", 'w')) 20 | $logger.level = Logger::DEBUG 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/client-api/request.rb: -------------------------------------------------------------------------------- 1 | require "net/http" 2 | require 'openssl' 3 | 4 | module ClientApi 5 | 6 | class Request 7 | 8 | include ClientApi 9 | 10 | def initialize(scenario) 11 | @scenario = scenario 12 | $logger.debug("Requested scenario == '#{@scenario.description}'") if $logger 13 | end 14 | 15 | def get_request(url, options = {}) 16 | connect(url) 17 | pre_logger(:log_url => uri(url), :log_header => header(options), :log_method => 'GET') if $logger 18 | @http.get(uri(url).request_uri, initheader = header(options)) 19 | end 20 | 21 | def get_with_body_request(url, options = {}) 22 | body = options[:body] || {} 23 | connect(url) 24 | pre_logger(:log_url => uri(url), :log_header => header(options), :log_body => body, :log_method => 'GET') if $logger 25 | 26 | request = Net::HTTP::Get.new(uri(url)) 27 | request.body = body.to_json 28 | header(options).each { |key,value| request.add_field(key,value)} 29 | @http.request(request) 30 | end 31 | 32 | def post_request(url, options = {}) 33 | body = options[:body] || {} 34 | connect(url) 35 | pre_logger(:log_url => uri(url), :log_header => header(options), :log_body => body, :log_method => 'POST') if $logger 36 | @http.post(uri(url).path, body.to_json, initheader = header(options)) 37 | end 38 | 39 | def post_request_x(url, options = {}) 40 | body = options[:body] 41 | connect(url) 42 | 43 | request = Net::HTTP::Post.new(uri(url)) 44 | body['data'].each { |key,value| request.set_form([[key.to_s,File.open(value)]], body['type'])} 45 | final_header = header(options).delete_if{ |k,| ['Content-Type', 'content-type', 'Content-type', 'content-Type'].include? k } 46 | final_header.each { |key,value| request.add_field(key,value)} 47 | 48 | pre_logger(:log_url => uri(url), :log_header => header(options), :log_body => body, :log_method => 'POST') if $logger 49 | @http.request(request) 50 | end 51 | 52 | def delete_request(url, options = {}) 53 | connect(url) 54 | pre_logger(:log_url => uri(url), :log_header => header(options), :log_method => 'DELETE') if $logger 55 | @http.delete(uri(url).path, initheader = header(options)) 56 | end 57 | 58 | def delete_with_body_request(url, options = {}) 59 | body = options[:body] || {} 60 | connect(url) 61 | pre_logger(:log_url => uri(url), :log_header => header(options), :log_body => body, :log_method => 'GET') if $logger 62 | 63 | request = Net::HTTP::Delete.new(uri(url)) 64 | request.body = body.to_json 65 | header(options).each { |key,value| request.add_field(key,value)} 66 | @http.request(request) 67 | end 68 | 69 | def put_request(url, options = {}) 70 | body = options[:body] || {} 71 | connect(url) 72 | pre_logger(:log_url => uri(url), :log_header => header(options), :log_body => body, :log_method => 'PUT') if $logger 73 | @http.put(uri(url).path, body.to_json, initheader = header(options)) 74 | end 75 | 76 | def patch_request(url, options = {}) 77 | body = options[:body] || {} 78 | connect(url) 79 | pre_logger(:log_url => uri(url), :log_header => header(options), :log_body => body, :log_method => 'PATCH') if $logger 80 | @http.patch(uri(url).path, body.to_json, initheader = header(options)) 81 | end 82 | 83 | def uri(args) 84 | if (args.include? "http://") || (args.include? "https://") 85 | URI.parse(args) 86 | else 87 | base_url = base_url_definition(args) 88 | URI.parse(base_url + args) 89 | end 90 | end 91 | 92 | def base_url_definition(args) 93 | raise "Invalid (or) incomplete URL: #{base_url + args}" unless (['https://', 'http://'].any? { |e| (base_url + args).include? e }) 94 | 95 | if (base_url[-1, 1] == '/') && (args[0] == '/') 96 | base_url.gsub(/\/$/, '') 97 | elsif (base_url[-1, 1] != '/') && (args[0] != '/') 98 | base_url.concat('', '/') 99 | else 100 | base_url 101 | end 102 | end 103 | 104 | def connect(args) 105 | http = Net::HTTP.new(uri(args).host, uri(args).port) 106 | 107 | if uri(args).scheme == "https" 108 | http.use_ssl = true 109 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 110 | http.read_timeout = time_out.to_i 111 | @http = http 112 | elsif uri(args).scheme == "http" 113 | http.use_ssl = false 114 | http.read_timeout = time_out.to_i 115 | @http = http 116 | end 117 | end 118 | 119 | def basic_encode(options = {}) 120 | 'Basic ' + ["#{options[:username]}:#{options[:password]}"].pack('m0') 121 | end 122 | 123 | def header(options = {}) 124 | mod_headers = options[:headers] || {} 125 | authorization = basic_encode(:username => basic_auth['Username'], :password => basic_auth['Password']) 126 | if headers == nil || headers == "" 127 | @headers = {} 128 | else 129 | @headers = headers 130 | end 131 | @headers['Authorization'] = authorization if authorization != "Basic Og==" 132 | @headers.merge(mod_headers) 133 | end 134 | 135 | def pre_logger(options = {}) 136 | options[:log_body] = 'not available' if options[:log_body].nil? 137 | $logger.debug("Requested method == #{options[:log_method]}") 138 | $logger.debug("Requested url == #{options[:log_url]}") 139 | $logger.debug("Requested headers == #{options[:log_header]}") 140 | $logger.debug("Requested body == #{options[:log_body]}") 141 | end 142 | end 143 | 144 | end -------------------------------------------------------------------------------- /lib/client-api/settings.rb: -------------------------------------------------------------------------------- 1 | module ClientApi 2 | 3 | def self.configure 4 | RSpec.configure do |config| 5 | yield config 6 | end 7 | end 8 | 9 | def self.configuration 10 | RSpec.configuration 11 | end 12 | 13 | def base_url 14 | ClientApi.configuration.base_url || '' 15 | end 16 | 17 | def headers 18 | ClientApi.configuration.headers || '' 19 | end 20 | 21 | def basic_auth 22 | ClientApi.configuration.basic_auth || '' 23 | end 24 | 25 | def json_output 26 | ClientApi.configuration.json_output || '' 27 | end 28 | 29 | def time_out 30 | ClientApi.configuration.time_out || 60 31 | end 32 | 33 | end -------------------------------------------------------------------------------- /lib/client-api/validator.rb: -------------------------------------------------------------------------------- 1 | require "json-schema" 2 | 3 | module ClientApi 4 | 5 | def validate(res, *options) 6 | 7 | # noinspection RubyScope 8 | options.map do |data| 9 | raise('"key": ""'.green + ' field is missing'.red) if data[:key].nil? 10 | raise('Need at least one field to validate'.green) if data[:value].nil? && data[:type].nil? && data[:size].nil? && data[:empty].nil? && data[:has_key].nil? 11 | 12 | data.keys.map do |val| 13 | raise "invalid key: " + "#{val}".green unless [:key, :operator, :value, :type, :size, :has_key, :empty].include? val 14 | end 15 | 16 | value ||= data[:value] 17 | operator ||= (data[:operator] || '==').downcase 18 | type ||= data[:type] 19 | size ||= data[:size] 20 | empty ||= data[:empty] 21 | has_key ||= data[:has_key] 22 | 23 | @resp = JSON.parse(res.to_json) 24 | key = data[:key].split("->") 25 | 26 | @err_method = [] 27 | 28 | key.map do |method| 29 | method = method.to_i if is_num?(method) 30 | @err_method = @err_method << method 31 | 32 | begin 33 | if (method.is_a? Integer) && (has_key != nil) 34 | @valid_key = false 35 | elsif (!method.is_a? Integer) && (has_key != nil) 36 | @valid_key = @resp.has_key? method 37 | elsif (method.is_a? Integer) && has_key.nil? 38 | raise %[key: ]+ %[#{@err_method.join('->')}].green + %[ does not exist! please check the key once again].red if ((@resp.class != Array) || (method.to_i >= @resp.count)) 39 | elsif (method.is_a? String) && has_key.nil? 40 | raise %[key: ]+ %[#{@err_method.join('->')}].green + %[ does not exist! please check the key once again].red if !@resp.include?(method) 41 | end 42 | @resp = @resp.send(:[], method) 43 | rescue NoMethodError 44 | raise %[key: ]+ %[#{@err_method.join('->')}].green + %[ does not exist! please check the key once again].red if has_key.nil? 45 | end 46 | end 47 | 48 | case operator 49 | when '=', '==', 'eql', 'eql?', 'equal', 'equal?' 50 | # has key validation 51 | validate_key(@valid_key, has_key, data) if has_key != nil 52 | 53 | # value validation 54 | expect(value).to eq(@resp), lambda {"[key]: \"#{data[:key]}\"".blue + "\n didn't match \n[value]: \"#{data[:value]}\"\n"} if value != nil 55 | 56 | # datatype validation 57 | if (type == 'boolean') || (type == 'bool') 58 | expect(%w[TrueClass, FalseClass].any? {|bool| bool.include? @resp.class.to_s}).to eq(true), lambda {"[key]: \"#{data[:key]}\"".blue + "\n datatype shouldn't be \n[type]: \"#{data[:type]}\"\n"} 59 | elsif ((type != 'boolean') || (type != 'bool')) && (type != nil) 60 | expect(datatype(type, value)).to eq(@resp.class), lambda {"[key]: \"#{data[:key]}\"".blue + "\n datatype shouldn't be \n[type]: \"#{data[:type]}\"\n"} 61 | end 62 | 63 | # size validation 64 | validate_size(size, data) if size != nil 65 | 66 | # is empty validation 67 | validate_empty(empty, data) if empty != nil 68 | 69 | when '!', '!=', '!eql', '!eql?', 'not eql', 'not equal', '!equal?' 70 | # has key validation 71 | validate_key(@valid_key, has_key, data) if has_key != nil 72 | 73 | # value validation 74 | expect(value).not_to eq(@resp), lambda {"[key]: \"#{data[:key]}\"".blue + "\n didn't match \n[value]: \"#{data[:value]}\"\n"} if value != nil 75 | 76 | # datatype validation 77 | if (type == 'boolean') || (type == 'bool') 78 | expect(%w[TrueClass, FalseClass].any? {|bool| bool.include? @resp.class.to_s}).not_to eq(true), lambda {"[key]: \"#{data[:key]}\"".blue + "\n datatype shouldn't be \n[type]: \"#{data[:type]}\"\n"} 79 | elsif ((type != 'boolean') || (type != 'bool')) && (type != nil) 80 | expect(datatype(type, value)).not_to eq(@resp.class), lambda {"[key]: \"#{data[:key]}\"".blue + "\n datatype shouldn't be \n[type]: \"#{data[:type]}\"\n"} 81 | end 82 | 83 | # size validation 84 | if size != nil 85 | begin 86 | expect(size).not_to eq(@resp.count), lambda {"[key]: \"#{data[:key]}\"".blue + "\n" + "size shouldn't match" + "\n\n" + "[ actual size ]: #{@resp.count}" + "\n" + "[size not expected]: #{size}".green} 87 | rescue NoMethodError 88 | expect(size).not_to eq(0), lambda {"[key]: \"#{data[:key]}\"".blue + "\n" + "size shouldn't match" + "\n\n" + "[ actual size ]: 0" + "\n" + "[size not expected]: #{size}".green} 89 | rescue ArgumentError 90 | expect(size).not_to eq(0), lambda {"[key]: \"#{data[:key]}\"".blue + "\n" + "size shouldn't match" + "\n\n" + "[ actual size ]: 0" + "\n" + "[size not expected]: #{size}".green} 91 | end 92 | end 93 | 94 | # is empty validation 95 | validate_empty(empty, data) if empty != nil 96 | 97 | when '>', '>=', '<', '<=', 'greater than', 'greater than or equal to', 'less than', 'less than or equal to', 'lesser than', 'lesser than or equal to' 98 | message = 'is not greater than (or) equal to' if operator == '>=' || operator == 'greater than or equal to' 99 | message = 'is not greater than' if operator == '>' || operator == 'greater than' 100 | message = 'is not lesser than (or) equal to' if operator == '<=' || operator == 'less than or equal to' 101 | message = 'is not lesser than' if operator == '<' || operator == 'less than' || operator == 'lesser than' 102 | 103 | # has key validation 104 | validate_key(@valid_key, has_key, data) if has_key != nil 105 | 106 | # value validation 107 | expect(@resp.to_i.public_send(operator, value)).to eq(true), "[key]: \"#{data[:key]}\"".blue + "\n #{message} \n[value]: \"#{data[:value]}\"\n" if value != nil 108 | 109 | # datatype validation 110 | expect(datatype(type, value)).to eq(@resp.class), lambda {"[key]: \"#{data[:key]}\"".blue + "\n datatype shouldn't be \n[type]: \"#{data[:type]}\"\n"} if type != nil 111 | 112 | # size validation 113 | if size != nil 114 | begin 115 | expect(@resp.count.to_i.public_send(operator, size)).to eq(true), lambda {"[key]: \"#{data[:key]}\"".blue + "\n" + "#{message} #{size}" + "\n\n" + "[ actual size ]: #{@resp.count}" + "\n" + "expected size to be #{operator} #{size}".green} 116 | rescue NoMethodError 117 | expect(0.public_send(operator, size)).to eq(true), lambda {"[key]: \"#{data[:key]}\"".blue + "\n" + "#{message} #{size}" + "\n\n" + "[ actual size ]: 0" + "\n" + "expected size to be #{operator} #{size}".green} 118 | rescue ArgumentError 119 | expect(0.public_send(operator, size)).to eq(true), lambda {"[key]: \"#{data[:key]}\"".blue + "\n" + "#{message} #{size}" + "\n\n" + "[ actual size ]: 0" + "\n" + "expected size to be #{operator} #{size}".green} 120 | end 121 | end 122 | 123 | # is empty validation 124 | validate_empty(empty, data) if empty != nil 125 | 126 | when 'contains', 'has', 'contains?', 'has?', 'include', 'include?' 127 | # has key validation 128 | validate_key(@valid_key, has_key, data) if has_key != nil 129 | 130 | # value validation 131 | expect(@resp.to_s).to include(value.to_s), lambda {"[key]: \"#{data[:key]} => #{@resp}\"".blue + "\n not contains \n[value]: \"#{data[:value]}\"\n"} if value != nil 132 | 133 | # datatype validation 134 | if (type == 'boolean') || (type == 'bool') 135 | expect(%w[TrueClass, FalseClass].any? {|bool| bool.include? @resp.class.to_s}).to eq(true), lambda {"[key]: \"#{data[:key]}\"".blue + "\n datatype shouldn't be \n[type]: \"#{data[:type]}\"\n"} 136 | elsif ((type != 'boolean') || (type != 'bool')) && (type != nil) 137 | expect(datatype(type, value)).to eq(@resp.class), lambda {"[key]: \"#{data[:key]}\"".blue + "\n datatype shouldn't be \n[type]: \"#{data[:type]}\"\n"} 138 | end 139 | 140 | # size validation 141 | if size != nil 142 | begin 143 | expect(@resp.count.to_s).to include(size.to_s), lambda {"[key]: \"#{data[:key]} => #{@resp}\"".blue + "\n"+ "size not contains #{size}"+ "\n\n" + "[ actual size ]: #{@resp.count}" + "\n" + "expected size to contain: #{size}".green} 144 | rescue NoMethodError 145 | expect(0.to_s).to include(size.to_s), lambda {"[key]: \"#{data[:key]} => #{@resp}\"".blue + "\n"+ "size not contains #{size}"+ "\n\n" + "[ actual size ]: 0" + "\n" + "expected size to contain: #{size}".green} 146 | rescue ArgumentError 147 | expect(0.to_s).to include(size.to_s), lambda {"[key]: \"#{data[:key]} => #{@resp}\"".blue + "\n"+ "size not contains #{size}"+ "\n\n" + "[ actual size ]: 0" + "\n" + "expected size to contain: #{size}".green} 148 | end 149 | end 150 | 151 | # is empty validation 152 | validate_empty(empty, data) if empty != nil 153 | 154 | when 'not contains', '!contains', 'not include', '!include' 155 | # has key validation 156 | validate_key(@valid_key, has_key, data) if has_key != nil 157 | 158 | # value validation 159 | expect(@resp.to_s).not_to include(value.to_s), lambda {"[key]: \"#{data[:key]} => #{@resp}\"".blue + "\n should not contain \n[value]: \"#{data[:value]}\"\n"} if value != nil 160 | 161 | # datatype validation 162 | if (type == 'boolean') || (type == 'bool') 163 | expect(%w[TrueClass, FalseClass].any? {|bool| bool.include? @resp.class.to_s}).not_to eq(true), lambda {"[key]: \"#{data[:key]}\"".blue + "\n datatype shouldn't be \n[type]: \"#{data[:type]}\"\n"} 164 | elsif ((type != 'boolean') || (type != 'bool')) && (type != nil) 165 | expect(datatype(type, value)).not_to eq(@resp.class), lambda {"[key]: \"#{data[:key]}\"".blue + "\n datatype shouldn't be \n[type]: \"#{data[:type]}\"\n"} 166 | end 167 | 168 | # size validation 169 | if size != nil 170 | begin 171 | expect(@resp.count.to_s).not_to include(size.to_s), lambda {"[key]: \"#{data[:key]} => #{@resp}\"".blue + "\n"+ "size should not contain #{size}"+ "\n\n" + "[ actual size ]: #{@resp.count}" + "\n" + "expected size not to contain: #{size}".green} 172 | rescue NoMethodError 173 | expect(0.to_s).not_to include(size.to_s), lambda {"[key]: \"#{data[:key]} => #{@resp}\"".blue + "\n"+ "size should not contain #{size}"+ "\n\n" + "[ actual size ]: 0" + "\n" + "expected size not to contain: #{size}".green} 174 | rescue ArgumentError 175 | expect(0.to_s).not_to include(size.to_s), lambda {"[key]: \"#{data[:key]} => #{@resp}\"".blue + "\n"+ "size should not contain #{size}"+ "\n\n" + "[ actual size ]: 0" + "\n" + "expected size not to contain: #{size}".green} 176 | end 177 | end 178 | 179 | # is empty validation 180 | validate_empty(empty, data) if empty != nil 181 | 182 | else 183 | raise('operator not matching') 184 | end 185 | 186 | end 187 | 188 | end 189 | 190 | def validate_list(res, *options) 191 | options.map do |data| 192 | 193 | raise('"sort": ""'.green + ' field is missing'.red) if data[:sort].nil? 194 | 195 | sort ||= data[:sort] 196 | unit ||= data[:unit].split("->") 197 | 198 | @value = [] 199 | @resp = JSON.parse(res.to_json) 200 | 201 | key = data[:key].split("->") 202 | 203 | key.map do |method| 204 | @resp = @resp.send(:[], method) 205 | end 206 | 207 | @cls = [] 208 | @resp.map do |val| 209 | unit.map do |list| 210 | val = val.send(:[], list) 211 | end 212 | @value << val 213 | begin 214 | @cls << (val.scan(/^\d+$/).any? ? Integer : val.class) 215 | rescue NoMethodError 216 | @cls << val.class 217 | end 218 | end 219 | 220 | @value = 221 | if @cls.all? {|e| e == Integer} 222 | @value.map(&:to_i) 223 | elsif (@cls.all? {|e| e == String}) || (@cls.include? String) 224 | @value.map(&:to_s) 225 | else 226 | @value 227 | end 228 | expect(@value).to eq(@value.sort) if sort == 'ascending' 229 | expect(@value).to eq(@value.sort.reverse) if sort == 'descending' 230 | 231 | end 232 | end 233 | 234 | def validate_size(size, data) 235 | begin 236 | expect(size).to eq(@resp.count), lambda {"[key]: \"#{data[:key]}\"".blue + "\n" + "size didn't match" + "\n\n" + "[ actual size ]: #{@resp.count}" + "\n" + "[expected size]: #{size}".green} 237 | rescue NoMethodError 238 | expect(size).to eq(0), lambda {"[key]: \"#{data[:key]}\"".blue + "\n" + "size didn't match" + "\n\n" + "[ actual size ]: 0" + "\n" + "[expected size]: #{size}".green} 239 | rescue ArgumentError 240 | expect(size).to eq(0), lambda {"[key]: \"#{data[:key]}\"".blue + "\n" + "size didn't match" + "\n\n" + "[ actual size ]: 0" + "\n" + "[expected size]: #{size}".green} 241 | end 242 | end 243 | 244 | def validate_empty(empty, data) 245 | case empty 246 | when true 247 | expect(@resp.nil?).to eq(empty), lambda {"[key]: \"#{data[:key]}\"".blue + "\n is not empty"+"\n"} if [Integer, TrueClass, FalseClass, Float].include? @resp.class 248 | expect(@resp.empty?).to eq(empty), lambda {"[key]: \"#{data[:key]}\"".blue + "\n is not empty"+"\n"} if [String, Hash, Array].include? @resp.class 249 | when false 250 | expect(@resp.nil?).to eq(empty), lambda {"[key]: \"#{data[:key]}\"".blue + "\n is empty"+"\n"} if [Integer, TrueClass, FalseClass, Float].include? @resp.class 251 | expect(@resp.empty?).to eq(empty), lambda {"[key]: \"#{data[:key]}\"".blue + "\n is empty"+"\n"} if [String, Hash, Array].include? @resp.class 252 | end 253 | end 254 | 255 | def validate_key(valid_key, has_key, data) 256 | case valid_key 257 | when true 258 | expect(valid_key).to eq(true), lambda {"[key]: \"#{data[:key]}\"".blue + "\n does not exist"+"\n"} if has_key == true 259 | expect(valid_key).to eq(false), lambda {"[key]: \"#{data[:key]}\"".blue + "\n does exist"+"\n"} if has_key == false 260 | when false 261 | expect(@resp.nil?).to eq(false), lambda {"[key]: \"#{data[:key]}\"".blue + "\n does not exist"+"\n"} if has_key == true 262 | expect(@resp.nil?).to eq(true), lambda {"[key]: \"#{data[:key]}\"".blue + "\n does exist"+"\n"} if has_key == false 263 | end 264 | end 265 | 266 | def validate_schema(param1, param2) 267 | expected_schema = JSON::Validator.validate(param1, param2) 268 | expect(expected_schema).to eq(true) 269 | end 270 | 271 | def validate_headers(res, *options) 272 | options.map do |data| 273 | raise('key is not given!') if data[:key].nil? 274 | raise('operator is not given!') if data[:operator].nil? 275 | raise('value is not given!') if data[:value].nil? 276 | 277 | @resp_header = JSON.parse(res.to_json) 278 | 279 | key = data[:key] 280 | value = data[:value] 281 | operator = data[:operator] 282 | 283 | case operator 284 | when '=', '==', 'eql?', 'equal', 'equal?' 285 | expect(value).to eq(@resp_header[key][0]), lambda {"\"#{key}\" => \"#{value}\"".blue + "\n didn't match \n\"#{key}\" => \"#{@resp_header[key][0]}\"\n"} 286 | 287 | when '!', '!=', '!eql?', 'not equal', '!equal?' 288 | expect(value).not_to eq(@resp_header[key][0]), lambda {"\"#{key}\" => \"#{value}\"".blue + "\n shouldn't match \n\"#{key}\" => \"#{@resp_header[key][0]}\"\n"} 289 | 290 | else 291 | raise('operator not matching') 292 | end 293 | end 294 | end 295 | 296 | def datatype(type, value) 297 | if (type.downcase == 'string') || (type.downcase.== 'str') 298 | String 299 | elsif (type.downcase.== 'integer') || (type.downcase.== 'int') 300 | Integer 301 | elsif (type.downcase == 'symbol') || (type.downcase == 'sym') 302 | Symbol 303 | elsif (type.downcase == 'array') || (type.downcase == 'arr') 304 | Array 305 | elsif (type.downcase == 'object') || (type.downcase == 'obj') 306 | Object 307 | elsif (type.downcase == 'boolean') || (type.downcase == 'bool') 308 | value === true ? TrueClass : FalseClass 309 | elsif (type.downcase == 'falseclass') || (type.downcase == 'false') 310 | FalseClass 311 | elsif (type.downcase == 'trueclass') || (type.downcase == 'true') 312 | TrueClass 313 | elsif type.downcase == 'float' 314 | Float 315 | elsif type.downcase == 'hash' 316 | Hash 317 | elsif type.downcase == 'complex' 318 | Complex 319 | elsif type.downcase == 'rational' 320 | Rational 321 | elsif type.downcase == 'fixnum' 322 | Fixnum 323 | elsif type.downcase == 'bignum' 324 | Bignum 325 | else 326 | end 327 | end 328 | 329 | def is_num?(str) 330 | if Float(str) 331 | true 332 | end 333 | rescue ArgumentError, TypeError 334 | false 335 | end 336 | 337 | def validate_json(actual, expected) 338 | param1 = JSON.parse(actual.to_json) 339 | param2 = JSON.parse(expected.to_json) 340 | 341 | @actual_key, @actual_value = [], [] 342 | deep_traverse(param2) do |path, value| 343 | if !value.is_a?(Hash) 344 | key_path = path.map! {|k| k} 345 | @actual_key << key_path.join("->").to_s 346 | @actual_value << value 347 | end 348 | end 349 | 350 | Hash[@actual_key.zip(@actual_value)].map do |data| 351 | @resp = param1 352 | key = data[0].split("->") 353 | 354 | key.map do |method| 355 | method = method.to_i if is_num?(method) 356 | @resp = @resp.send(:[], method) 357 | end 358 | 359 | value = data[1] 360 | @assert, @final_assert, @overall = [], [], [] 361 | 362 | if !value.is_a?(Array) 363 | expect(value).to eq(@resp) 364 | else 365 | @resp.each_with_index do |resp, i| 366 | value.to_a.each_with_index do |val1, j| 367 | val1.to_a.each_with_index do |val2, k| 368 | if resp.to_a.include? val2 369 | @assert << true 370 | else 371 | @assert << false 372 | end 373 | end 374 | @final_assert << @assert 375 | @assert = [] 376 | 377 | if @resp.count == @final_assert.count 378 | @final_assert.each_with_index do |result, i| 379 | if result.count(true) == val1.count 380 | @overall << true 381 | break 382 | elsif @final_assert.count == i+1 383 | expect(value).to eq(@resp) 384 | end 385 | end 386 | @final_assert = [] 387 | end 388 | 389 | end 390 | end 391 | 392 | if @overall.count(true) == value.count 393 | else 394 | expect(value).to eq(@resp) 395 | end 396 | 397 | end 398 | end 399 | end 400 | 401 | def deep_traverse(hash, &block) 402 | stack = hash.map {|k, v| [[k], v]} 403 | 404 | while not stack.empty? 405 | key, value = stack.pop 406 | yield(key, value) 407 | if value.is_a? Hash 408 | value.each do |k, v| 409 | if v.is_a?(String) then 410 | if v.empty? then 411 | v = "" 412 | end 413 | end 414 | stack.push [key.dup << k, v] 415 | end 416 | end 417 | end 418 | end 419 | 420 | end 421 | 422 | class String 423 | def black; "\e[30m#{self}\e[0m" end 424 | def red; "\e[31m#{self}\e[0m" end 425 | def green; "\e[32m#{self}\e[0m" end 426 | def brown; "\e[33m#{self}\e[0m" end 427 | def blue; "\e[34m#{self}\e[0m" end 428 | def magenta; "\e[35m#{self}\e[0m" end 429 | def cyan; "\e[36m#{self}\e[0m" end 430 | def gray; "\e[37m#{self}\e[0m" end 431 | 432 | def bold; "\e[1m#{self}\e[22m" end 433 | def italic; "\e[3m#{self}\e[23m" end 434 | def underline; "\e[4m#{self}\e[24m" end 435 | def blink; "\e[5m#{self}\e[25m" end 436 | def reverse_color; "\e[7m#{self}\e[27m" end 437 | end -------------------------------------------------------------------------------- /lib/client-api/version.rb: -------------------------------------------------------------------------------- 1 | module ClientApi 2 | VERSION = "0.4.1".freeze 3 | end 4 | -------------------------------------------------------------------------------- /spec/client/basic_auth_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Basic authentication' do 2 | 3 | it "basic auth", :post do 4 | api = ClientApi::Api.new 5 | api.post('https://api.enterprise.apigee.com/v1/organizations/ahamilton-eval',{}) 6 | 7 | expect(api.status).to eq(403) 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /spec/client/custom_header_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Custom header' do 2 | 3 | it "custom header", :post do 4 | api = ClientApi::Api.new 5 | api.post('https://api.enterprise.apigee.com/v1/organizations/ahamilton-eval',{},{'Authorization' => 'Basic YWhhbWlsdG9uQGFwaWdlZS5jb206bXlwYXNzdzByZAo'}) 6 | 7 | expect(api.status).to eq(403) 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /spec/client/default_validation_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Default validation' do 2 | 3 | it "boolean datatype validator", :get do 4 | api = ClientApi::Api.new 5 | api.get('https://jsonplaceholder.typicode.com/todos/1') 6 | 7 | expect(api.status).to eq(200) 8 | validate( 9 | api.body, 10 | { 11 | "key": "completed", 12 | "value": false, 13 | "operator": "==", 14 | "type": 'boolean' 15 | } 16 | ) 17 | end 18 | 19 | it "get with json input", :get do 20 | api = ClientApi::Api.new 21 | api.get_with_body('http://jservice.io/api/categories', { "count": 2 }, {'Content-Type' => 'application/json', 'Accept' => 'application/json'}) 22 | 23 | expect(api.status).to eq(200) 24 | validate( 25 | api.body, 26 | { 27 | "key": "1->id", 28 | "operator": "==", 29 | "type": 'integer' 30 | } 31 | ) 32 | end 33 | 34 | it "multi key-pair response validator", :post do 35 | api = ClientApi::Api.new 36 | api.post('/api/users', payload("./data/request/post.json")) 37 | 38 | expect(api.status).to eq(201) 39 | validate( 40 | api.body, 41 | { 42 | "key": "name", 43 | "value": "prashanth sams", 44 | "operator": "==", 45 | "type": 'string' 46 | }, 47 | { 48 | "key": "id", 49 | "operator": "!=", 50 | "type": 'integer' 51 | } 52 | ) 53 | 54 | validate(api.body, {"key": "id", "operator": "eql?", "type": 'string'}) 55 | end 56 | 57 | it "multi key-pair response validator - json tree", :get do 58 | api = ClientApi::Api.new 59 | api.get('https://my-json-server.typicode.com/typicode/demo/db') 60 | 61 | expect(api.status).to eq(200) 62 | validate( 63 | api.body, 64 | { 65 | "key": "profile->name", 66 | "value": "typicode", 67 | "operator": "==", 68 | "type": 'string' 69 | }, 70 | { 71 | "key": "posts->1->id", 72 | "value": 2, 73 | "operator": "==", 74 | "type": 'integer' 75 | } 76 | ) 77 | end 78 | 79 | it "greater/lesser than response validator - json tree" do 80 | actual = 81 | { 82 | "posts": [ 83 | { 84 | "id": 3, 85 | "title": "Post 3" 86 | } 87 | ], 88 | "profile": { 89 | "name": "typicode" 90 | } 91 | } 92 | 93 | validate( 94 | actual, 95 | { 96 | "key": "posts->0->id", 97 | "operator": "<=", 98 | "value": 5, 99 | "type": "integer" 100 | } 101 | ) 102 | end 103 | 104 | it "not contains/contains response validator - json tree" do 105 | actual = 106 | { 107 | "post1": [ 108 | { 109 | "name": "Prashanth Sams", 110 | "title": "Post 1", 111 | "available": true 112 | } 113 | ], 114 | "post2": [ 115 | { 116 | "id": 434, 117 | "title": "Post 2" 118 | } 119 | ] 120 | } 121 | 122 | validate( 123 | actual, 124 | { 125 | "key": "post1->0->name", 126 | "operator": "contains", 127 | "value": "Sams" 128 | },{ 129 | "key": "post1->0->name", 130 | "operator": "include", 131 | "value": "Sams" 132 | }, 133 | { 134 | "key": "post2->0->id", 135 | "operator": "contains", 136 | "value": 34, 137 | "type": 'integer' 138 | }, 139 | { 140 | "key": "post1->0->name", 141 | "operator": "not contains", 142 | "value": "Samuel" 143 | }, 144 | { 145 | "key": "post2->0->id", 146 | "operator": "!include", 147 | "value": 33, 148 | "type": "string" 149 | }, 150 | { 151 | "key": "post1->0->available", 152 | "value": true, 153 | "operator": "contains", 154 | "type": "boolean" 155 | } 156 | ) 157 | end 158 | 159 | it "has_key? validator - json tree" do 160 | actual = 161 | { 162 | "post1": [ 163 | { 164 | "name": "Prashanth Sams", 165 | "title": "Post 1", 166 | "available": true 167 | } 168 | ], 169 | "post2": [ 170 | { 171 | "id": 434, 172 | "title": "Post 2" 173 | } 174 | ] 175 | } 176 | 177 | validate( 178 | actual, 179 | { 180 | "key": "post1", 181 | "has_key": true 182 | }, 183 | { 184 | "key": "posts1", 185 | "has_key": false 186 | }, 187 | { 188 | "key": "post1->0", 189 | "has_key": true 190 | }, 191 | { 192 | "key": "post1->2", 193 | "has_key": false 194 | }, 195 | { 196 | "key": "post1->1->0->name", 197 | "has_key": false 198 | }, 199 | { 200 | "key": "post1->1->name", 201 | "has_key": false 202 | }, 203 | { 204 | "key": "post1->0->name", 205 | "operator": "==", 206 | "value": "Prashanth Sams", 207 | "has_key": true 208 | }, 209 | { 210 | "key": "post1->0->name", 211 | "operator": "==", 212 | "value": "Prashanth Sams", 213 | "type": "string", 214 | "has_key": true 215 | }, 216 | { 217 | "key": "post1->0->name", 218 | "operator": "==", 219 | "value": "Prashanth Sams", 220 | "type": "string", 221 | "size": 0, 222 | "has_key": true 223 | }, 224 | { 225 | "key": "post1->0->name", 226 | "operator": "==", 227 | "value": "Prashanth Sams", 228 | "type": "string", 229 | "size": 0, 230 | "empty": false, 231 | "has_key": true 232 | } 233 | ) 234 | end 235 | 236 | it "has_key? validator x2- json tree" do 237 | actual = { 238 | "posts": [ 239 | { 240 | "0": 1, 241 | "title": "Post 1" 242 | },{ 243 | "1": 2, 244 | "title": "Post 2" 245 | },{ 246 | "2": 3, 247 | "title": "Post 3" 248 | },{ 249 | "3": 4, 250 | "title": "Post 4" 251 | },{ 252 | "4": 5, 253 | "title": "Post 5" 254 | } 255 | ], 256 | "profile": { 257 | "name": "typicode" 258 | } 259 | } 260 | 261 | validate( 262 | actual, 263 | { 264 | 'key': 'profile->name', 265 | 'has_key': true 266 | }, 267 | { 268 | 'key': 'profile->nasme', 269 | 'has_key': false 270 | }, 271 | { 272 | 'key': 'posts->7', 273 | 'has_key': false 274 | }, 275 | { 276 | 'key': 'posts->0', 277 | 'has_key': true 278 | } 279 | ) 280 | end 281 | 282 | it "operator == as optional validator - json tree" do 283 | actual = 284 | { 285 | "post1": [ 286 | { 287 | "name": "Prashanth Sams", 288 | "title": "Post 1", 289 | "available": true 290 | } 291 | ], 292 | "post2": [ 293 | { 294 | "id": 434, 295 | "title": "Post 2" 296 | } 297 | ] 298 | } 299 | 300 | validate( 301 | actual, 302 | { 303 | "key": "post1->0->name", 304 | "value": "Prashanth Sams", 305 | "type": "string", 306 | "size": 0, 307 | "empty": false, 308 | "has_key": true 309 | }, 310 | { 311 | "key": "post1->0->name", 312 | "type": "string", 313 | "size": 0, 314 | "empty": false, 315 | "has_key": true 316 | }, 317 | { 318 | "key": "post1->0->name", 319 | "size": 0, 320 | "empty": false, 321 | "has_key": true 322 | }, 323 | { 324 | "key": "post1->0->name", 325 | "empty": false, 326 | "has_key": true 327 | }, 328 | { 329 | "key": "post1->0->name", 330 | "has_key": true 331 | }, 332 | { 333 | "key": "post1->0->name", 334 | "value": "Prashanth Sams", 335 | "type": "string", 336 | "size": 0, 337 | "empty": false 338 | }, 339 | { 340 | "key": "post1->0->name", 341 | "value": "Prashanth Sams", 342 | "type": "string", 343 | "size": 0 344 | }, 345 | { 346 | "key": "post1->0->name", 347 | "value": "Prashanth Sams", 348 | "type": "string" 349 | }, 350 | { 351 | "key": "post1->0->name", 352 | "value": "Prashanth Sams" 353 | } 354 | ) 355 | end 356 | 357 | 358 | end 359 | -------------------------------------------------------------------------------- /spec/client/empty_validation_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Empty validation' do 2 | 3 | it "json response value empty? validator" do 4 | 5 | actual = 6 | [ 7 | { 8 | "id": 11510, 9 | "title": "", 10 | "clues_count": 5 11 | }, 12 | { 13 | "id": 11531, 14 | "title": "mixed bag", 15 | "clues_count": 5.0 16 | } 17 | ] 18 | 19 | validate( 20 | actual, 21 | { 22 | "key": "0->title", 23 | "empty": true 24 | }, 25 | { 26 | "key": "1->title", 27 | "empty": false 28 | }, 29 | { 30 | "key": "1->title", 31 | "operator": "==", 32 | "type": "string", 33 | "empty": false 34 | }, 35 | { 36 | "key": "1->title", 37 | "operator": "==", 38 | "value": "mixed bag", 39 | "empty": false 40 | }, 41 | { 42 | "key": "1->title", 43 | "operator": "==", 44 | "type": "string", 45 | "value": "mixed bag", 46 | "empty": false 47 | }, 48 | { 49 | "key": "1->title", 50 | "operator": "==", 51 | "type": "string", 52 | "value": "mixed bag", 53 | "size": 0, 54 | "empty": false 55 | }, 56 | { 57 | "key": "1", 58 | "empty": false 59 | }, 60 | { 61 | "key": "1->id", 62 | "empty": false 63 | }, 64 | { 65 | "key": "", 66 | "empty": false 67 | }, 68 | { 69 | "key": "1->clues_count", 70 | "empty": false 71 | } 72 | ) 73 | 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /spec/client/header_validation_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Header validation' do 2 | 3 | it "validate headers", :get do 4 | api = ClientApi::Api.new 5 | api.get('https://my-json-server.typicode.com/typicode/demo/db') 6 | 7 | expect(api.status).to eq(200) 8 | 9 | validate_headers( 10 | api.response_headers, 11 | { 12 | key: "connection", 13 | operator: "!=", 14 | value: "open" 15 | },{ 16 | key: "vary", 17 | operator: "==", 18 | value: "Origin, Accept-Encoding" 19 | } 20 | ) 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /spec/client/json_body_template.rb: -------------------------------------------------------------------------------- 1 | describe 'JSON template as body' do 2 | 3 | it "{POST request} json template as body", :post do 4 | api = ClientApi::Api.new 5 | api.post('/api/users', payload("./data/request/post.json")) 6 | 7 | expect(api.status).to eq(201) 8 | end 9 | end -------------------------------------------------------------------------------- /spec/client/json_schema_validation_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'JSON schema validation' do 2 | 3 | it "json response schema validator", :get do 4 | api = ClientApi::Api.new 5 | api.get('/api/users/2') 6 | 7 | expect(api.status).to eq(200) 8 | validate_schema( 9 | { 10 | "required": [ 11 | "data" 12 | ], 13 | "type": "object", 14 | "properties": { 15 | "data": { 16 | "type": "object", 17 | "required": [ 18 | "id", "email", "first_name", "last_name", "avatar" 19 | ], 20 | "properties": { 21 | "id": { 22 | "type": "integer" 23 | }, 24 | "email": { 25 | "type": "string" 26 | }, 27 | "first_name": { 28 | "type": "string" 29 | }, 30 | "last_name": { 31 | "type": "string" 32 | }, 33 | "avatar": { 34 | "type": "string" 35 | } 36 | } 37 | } 38 | } 39 | }, 40 | { 41 | "data": 42 | { 43 | "id": 2, 44 | "email": "janet.weaver@reqres.in", 45 | "first_name": "Janet", 46 | "last_name": "Weaver", 47 | "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg" 48 | } 49 | } 50 | ) 51 | 52 | validate_schema( 53 | schema_from_json('./data/schema/get_user_schema.json'), 54 | api.body 55 | ) 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /spec/client/json_value_validation_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'JSON value validation' do 2 | 3 | it "json response value structure validator", :get do 4 | api = ClientApi::Api.new 5 | api.get('https://my-json-server.typicode.com/typicode/demo/db') 6 | 7 | validate_json( 8 | api.body, 9 | { 10 | "posts": [ 11 | { 12 | "id": 2, 13 | "title": "Post 2" 14 | } 15 | ], 16 | "profile": { 17 | "name": "typicode" 18 | } 19 | } 20 | ) 21 | 22 | actual = { 23 | "posts": 24 | { 25 | "prashanth": { 26 | "id": 1, 27 | "title": [ 28 | { 29 | "post": 1, 30 | "ref": "ref-1" 31 | }, 32 | { 33 | "post": 2, 34 | "ref": "ref-2" 35 | }, 36 | { 37 | "post": 3, 38 | "ref": "ref-3" 39 | } 40 | ] 41 | }, 42 | "sams": { 43 | "id": 2, 44 | "title": "Post 2" 45 | }, 46 | "heera": { 47 | "id": 3, 48 | "title": "Post 3" 49 | } 50 | }, 51 | "profile": 52 | { 53 | "id": 44, 54 | "title": "Post 44" 55 | } 56 | } 57 | 58 | validate_json( 59 | actual, 60 | { 61 | "posts": 62 | { 63 | "prashanth": { 64 | "id": 1, 65 | "title": [ 66 | { 67 | "post": 1 68 | }, 69 | { 70 | "post": 2, 71 | "ref": "ref-2" 72 | } 73 | ] 74 | }, 75 | "heera": { 76 | "id": 3, 77 | "title": "Post 3" 78 | } 79 | }, 80 | "profile": 81 | { 82 | "id": 44, 83 | "title": "Post 44" 84 | } 85 | } 86 | ) 87 | end 88 | 89 | end -------------------------------------------------------------------------------- /spec/client/response_status_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Response and status validation' do 2 | 3 | it "{GET request} 200 response", :get do 4 | api = ClientApi::Api.new 5 | api.get('/api/users') 6 | 7 | expect(api.status).to eq(200) 8 | expect(api.code).to eq(200) 9 | expect(api.message).to eq('OK') 10 | end 11 | 12 | it "{POST request} 201 response", :post do 13 | api = ClientApi::Api.new 14 | api.post('/api/users', {"name": "prashanth sams"}) 15 | 16 | expect(api.status).to eq(201) 17 | end 18 | 19 | it "{DELETE request} 204 response", :delete do 20 | api = ClientApi::Api.new 21 | api.delete('/api/users/3') 22 | 23 | expect(api.status).to eq(204) 24 | end 25 | 26 | it "{PUT request} 200 response", :put do 27 | api = ClientApi::Api.new 28 | api.put('/api/users/2', {"data":{"email":"prashanth@mail.com","first_name":"Prashanth","last_name":"Sams"}}) 29 | 30 | expect(api.status).to eq(200) 31 | end 32 | 33 | it "{PATCH request} 200 response", :patch do 34 | api = ClientApi::Api.new 35 | api.patch('/api/users/2', {"data":{"email":"prashanth@mail.com","first_name":"Prashanth","last_name":"Sams"}}) 36 | 37 | expect(api.status).to eq(200) 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /spec/client/scheme_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'http(s) validation' do 2 | 3 | it "http scheme validator", :get do 4 | api = ClientApi::Api.new 5 | api.get('http://jservice.io/api/categories', {"Accept"=>"*/*"}) 6 | 7 | expect(api.status).to eq(200) 8 | validate( 9 | api.body, 10 | { 11 | "key": "0->id", 12 | "operator": "==", 13 | "type": 'int' 14 | } 15 | ) 16 | end 17 | 18 | end -------------------------------------------------------------------------------- /spec/client/size_validation_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Size validation' do 2 | 3 | it "json response size validator" do 4 | 5 | actual = 6 | { 7 | "posts": [ 8 | { 9 | "id": 1, 10 | "title": "Post 1" 11 | }, 12 | { 13 | "id": 2, 14 | "title": "Post 2" 15 | }, 16 | { 17 | "id": 3, 18 | "title": "Post 3" 19 | } 20 | ], 21 | "profile": { 22 | "name": [ 23 | { 24 | "type": "new", 25 | "more": [ 26 | "created": [ 27 | "really": { 28 | "finally": true 29 | } 30 | ], 31 | "created2": [ 32 | "really": { 33 | "finally": false 34 | } 35 | ] 36 | ] 37 | } 38 | ] 39 | } 40 | } 41 | 42 | validate( 43 | actual, 44 | { 45 | "key": "posts->0", 46 | "operator": "==", 47 | "size": 2, 48 | "type": 'hash' 49 | }, 50 | { 51 | "key": "posts->0->id", 52 | "operator": "==", 53 | "value": 1, 54 | "size": 0 55 | }, 56 | { 57 | "key": "posts->0->id", 58 | "operator": "not contains", 59 | "size": 1 60 | }, 61 | { 62 | "key": "posts->0->id", 63 | "operator": "contains", 64 | "size": 0 65 | }, 66 | { 67 | "key": "posts->1", 68 | "operator": "==", 69 | "type": "hash", 70 | "size": 2 71 | }, 72 | { 73 | "key": "posts->1", 74 | "operator": "<=", 75 | "type": "hash", 76 | "size": 2 77 | } 78 | ) 79 | 80 | end 81 | 82 | it "json response size validator x2" do 83 | 84 | actual = 85 | [ 86 | { 87 | "id": 11510, 88 | "title": "pair of dice, lost", 89 | "clues_count": 5 90 | }, 91 | { 92 | "id": 11531, 93 | "title": "mixed bag", 94 | "clues_count": 5 95 | } 96 | ] 97 | 98 | 99 | validate( 100 | actual, 101 | { 102 | "key": "", 103 | "operator": "==", 104 | "size": 2 105 | }, 106 | { 107 | "key": "1->title", 108 | "size": 0 109 | } 110 | ) 111 | 112 | end 113 | 114 | end 115 | -------------------------------------------------------------------------------- /spec/client/sorting_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Soring validation' do 2 | 3 | it "json response sorting validator - ascending" do 4 | 5 | actual = { 6 | "posts": [ 7 | { 8 | "id": 1, 9 | "int": "1", 10 | "title": "Post 1" 11 | },{ 12 | "id": 2, 13 | "int": "2", 14 | "title": "Post 2" 15 | },{ 16 | "id": 3, 17 | "int": "3", 18 | "title": "Post 3" 19 | },{ 20 | "id": 4, 21 | "int": "4", 22 | "title": "Post 4" 23 | },{ 24 | "id": 5, 25 | "int": "5", 26 | "title": "Post 5" 27 | } 28 | ], 29 | "profile": { 30 | "name": "typicode" 31 | } 32 | } 33 | 34 | validate_list( 35 | actual, 36 | { 37 | "key": "posts", 38 | "unit": "id", 39 | "sort": "ascending" 40 | },{ 41 | "key": "posts", 42 | "unit": "int", 43 | "sort": "ascending" 44 | } 45 | ) 46 | end 47 | 48 | it "json response sorting validator - descending" do 49 | 50 | actual = { 51 | "posts": [ 52 | { 53 | "id": 5, 54 | "title": "Post 5" 55 | },{ 56 | "id": 4, 57 | "title": "Post 4" 58 | },{ 59 | "id": 3, 60 | "title": "Post 3" 61 | },{ 62 | "id": 2, 63 | "title": "Post 2" 64 | },{ 65 | "id": 1, 66 | "title": "Post 1" 67 | } 68 | ], 69 | "profile": { 70 | "name": "typicode" 71 | } 72 | } 73 | 74 | validate_list( 75 | actual, 76 | { 77 | "key": "posts", 78 | "unit": "id", 79 | "sort": "descending" 80 | },{ 81 | "key": "posts", 82 | "unit": "id", 83 | "sort": "descending" 84 | } 85 | ) 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "client-api" 3 | require "rspec" 4 | require "rspec/expectations" 5 | require "json" 6 | 7 | RSpec.configure do |config| 8 | # Enable flags like --only-failures and --next-failure 9 | config.example_status_persistence_file_path = ".rspec_status" 10 | 11 | # Disable RSpec exposing methods globally on `Module` and `main` 12 | config.disable_monkey_patching! 13 | 14 | config.expect_with :rspec do |c| 15 | c.syntax = :expect 16 | end 17 | 18 | config.filter_run_when_matching :focus 19 | config.expose_dsl_globally = true 20 | end 21 | 22 | ClientApi.configure do |config| 23 | config.base_url = 'https://reqres.in' 24 | config.headers = {'Content-Type' => 'application/json', 'Accept' => 'application/json'} 25 | config.basic_auth = {'Username' => 'ahamilton@apigee.com', 'Password' => 'myp@ssw0rd'} 26 | config.json_output = {'Dirname' => './output', 'Filename' => 'test'} 27 | config.time_out = 10 # in secs 28 | config.logger = {'Dirname' => './logs', 'Filename' => 'test', 'StoreFilesCount' => 2} 29 | 30 | config.before(:each) do |scenario| 31 | ClientApi::Request.new(scenario) 32 | end 33 | end --------------------------------------------------------------------------------