├── .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 | [](http://badge.fury.io/rb/client-api)
4 | [](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 |
165 | General Syntax
166 | |
167 |
168 | Syntax | Model 2
169 | |
170 |
171 | Syntax | Model 3
172 | |
173 |
174 | Syntax | Model 4
175 | |
176 |
177 | Syntax | Model 5
178 | |
179 |
180 | Syntax | Model 6
181 | |
182 |
183 |
184 |
185 |
186 | validate(
187 | api.body,
188 | {
189 | key: '',
190 | operator: '',
191 | value: '',
192 | type: ''
193 | }
194 | )
195 |
196 | |
197 |
198 |
199 | validate(
200 | api.body,
201 | {
202 | key: '',
203 | size: 0
204 | }
205 | )
206 |
207 | |
208 |
209 |
210 | validate(
211 | api.body,
212 | {
213 | key: '',
214 | empty: true
215 | }
216 | )
217 |
218 | |
219 |
220 |
221 | validate(
222 | api.body,
223 | {
224 | key: '',
225 | has_key: true
226 | }
227 | )
228 |
229 | |
230 |
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 | |
246 |
247 |
248 | validate(
249 | api.body,
250 | {
251 | key: '',
252 | type: ''
253 | },
254 | {
255 | key: '',
256 | operator: '',
257 | value: ''
258 | }
259 | )
260 |
261 | |
262 |
263 |
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 |
278 | General Syntax
279 | |
280 |
281 | Syntax | Model 2
282 | |
283 |
284 |
285 |
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 | |
307 |
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 | |
323 |
324 |
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 |
334 | General Syntax
335 | |
336 |
337 | Syntax | Model 2
338 | |
339 |
340 |
341 |
342 |
343 | validate_list(
344 | api.body,
345 | {
346 | "key": "posts",
347 | "unit": "id",
348 | "sort": "ascending"
349 | }
350 | )
351 |
352 | |
353 |
354 |
355 | validate_list(
356 | api.body,
357 | {
358 | "key": "posts",
359 | "unit": "id",
360 | "sort": "descending"
361 | }
362 | )
363 |
364 | |
365 |
366 |
367 |
368 | #### JSON response headers validation
369 |
370 | > key benefits
371 | - validates any response headers
372 |
373 |
374 |
375 |
376 | General Syntax
377 | |
378 |
379 | Syntax | Model 2
380 | |
381 |
382 |
383 |
384 |
385 | validate_headers(
386 | api.response_headers,
387 | {
388 | key: '',
389 | operator: '',
390 | value: ''
391 | }
392 | )
393 |
394 | |
395 |
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 | |
411 |
412 |
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
--------------------------------------------------------------------------------