greenlight 0.0.1.pre.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 456a4b33aa62487e1ceff2c2f08c13ed707c966f
4
+ data.tar.gz: e76bba99e28a32911d5540e7b54b78223b0e38c4
5
+ SHA512:
6
+ metadata.gz: dcd8b0b3ce263b5d92e5ecba6d930df802e2f612179477487f7e11dff404d83a5d9f0e316a4c856e11fe57f2d0a87b85976e9ecdeda3d003a95b164e636c9bdc
7
+ data.tar.gz: 92e78e6c8e0ce65b19ee4e1c31dc6c53d76230b7a398d9d71b24358cb639bd7ad51111c4a5e6612c834230bfed2c5011e1e9098e72d47eb994cad805a6a28a97
@@ -0,0 +1,92 @@
1
+ require 'singleton'
2
+
3
+ class Colors
4
+
5
+ COLORS_LIB = {
6
+ :red => '31',
7
+ :green => '32',
8
+ :yellow => '33',
9
+ :blue => '34',
10
+ :grey => '37',
11
+ :magenta => '35',
12
+ :cyan => '36',
13
+ :light_cyan => '96',
14
+ :light_red => '91',
15
+ :light_blue => '94',
16
+ :light_magenta => '95',
17
+ :light_green => '92',
18
+ :light_yellow => '93',
19
+ :white => '97'
20
+
21
+ }
22
+ class << self
23
+ COLORS_LIB.each do |color_name, color_val|
24
+ define_method(color_name.to_s) { |msg|
25
+ $stdout.tty? ? "\e[#{color_val}m#{msg}\e[0m" : msg
26
+ }
27
+ end
28
+ end
29
+
30
+ end
31
+
32
+ class Console
33
+
34
+ include Singleton
35
+
36
+ attr_accessor :indent_level
37
+
38
+ INDENT_STR = ' '
39
+
40
+ def initialize
41
+ self.indent_level = 0
42
+ end
43
+
44
+ def info(msg)
45
+ puts Colors.grey(' - ' + get_indent + msg)
46
+ end
47
+
48
+ def action(msg)
49
+ puts ' * ' + get_indent + msg
50
+ end
51
+
52
+ def error(msg)
53
+ puts Colors.light_red(' ' + Console.utf8("\u2718") + ' ' + get_indent + msg)
54
+ end
55
+
56
+ def success(msg)
57
+ puts Colors.light_green(' ' + Console.utf8("\u2713") + ' ' + get_indent + msg)
58
+ end
59
+
60
+ def get_indent
61
+ INDENT_STR * indent_level
62
+ end
63
+
64
+ def indent
65
+ self.indent_level = self.indent_level + 1
66
+ end
67
+
68
+ def unindent
69
+ self.indent_level = self.indent_level - 1 unless self.indent_level == 0
70
+ end
71
+
72
+ def self.utf8(code)
73
+ code.encode('utf-8')
74
+ end
75
+ end
76
+
77
+
78
+ def info(msg)
79
+ Console.instance.info(msg)
80
+ end
81
+
82
+ def action(msg)
83
+ Console.instance.action(msg)
84
+ end
85
+
86
+ def error(msg)
87
+ Console.instance.error(msg)
88
+ end
89
+
90
+ def success(msg)
91
+ Console.instance.success(msg)
92
+ end
@@ -0,0 +1,25 @@
1
+ require 'singleton'
2
+
3
+ class Injector
4
+ include Singleton
5
+
6
+ attr_accessor :headers
7
+
8
+ def initialize
9
+ self.headers = {}
10
+ end
11
+
12
+ def add_header(header, value)
13
+ self.headers[header] = value
14
+ end
15
+
16
+ def rm_header(header)
17
+ self.headers.delete(header)
18
+ end
19
+
20
+ def self.decorate(options)
21
+ options[:headers] = {} unless (options.key?(:headers) && options[:headers].is_a?(Hash))
22
+ options[:headers].merge!(Injector.instance.headers)
23
+ options
24
+ end
25
+ end
@@ -0,0 +1,168 @@
1
+ require 'typhoeus'
2
+ require 'json'
3
+
4
+ require 'greenlight/injector'
5
+ require 'greenlight/console'
6
+
7
+ module Greenlight
8
+
9
+ class RequestException < StandardError; end
10
+ class AssertionException < StandardError; end
11
+
12
+ class RequestResponse
13
+ attr_accessor :body, :headers, :raw_body, :total_time, :code
14
+ end
15
+
16
+ class Request
17
+
18
+ # request description
19
+ attr_accessor :url, :options
20
+
21
+ # request response
22
+ attr_accessor :response
23
+
24
+ # response structure with json parsed body
25
+ attr_accessor :req_response
26
+
27
+ # request assertions
28
+ attr_accessor :expectations
29
+
30
+ # count assertions to report the index of the failed one
31
+ attr_accessor :assert_no
32
+
33
+
34
+ def initialize(url, options)
35
+ self.url = url
36
+ self.options = options
37
+ self.expectations = []
38
+ end
39
+
40
+ # define list of expectations (assertions)
41
+ def expect(&block)
42
+ self.expectations = block
43
+ run
44
+ end
45
+
46
+ def _debug_info
47
+ if code != 0
48
+ info "response status code: #{code}"
49
+ else
50
+ info "library returned: #{response.return_code}"
51
+ end
52
+ info "request body: #{options[:body]}"
53
+ info "request headers: #{options[:headers]}"
54
+ info "response body: #{body}"
55
+ info "response headers: #{response.headers}"
56
+
57
+ end
58
+
59
+ # define assertion
60
+ def assert(condition)
61
+ unless condition
62
+ error "assertion no. #{assert_no} failed"
63
+ _debug_info
64
+ raise AssertionException
65
+ end
66
+
67
+ self.assert_no = assert_no + 1
68
+ end
69
+
70
+ # assertion helpers
71
+ def header(name)
72
+ response.headers[name]
73
+ end
74
+
75
+ def headers
76
+ response.headers
77
+ end
78
+
79
+ def code
80
+ response.code
81
+ end
82
+
83
+ def raw_body
84
+ response.body
85
+ end
86
+
87
+ def body
88
+ req_response.body
89
+ end
90
+
91
+ def total_time
92
+ response.total_time
93
+ end
94
+
95
+ # run the request and evaluate expectations
96
+ def run
97
+
98
+ action Colors.grey("REQUEST ") + Colors.light_blue("#{options[:method].upcase} #{url}")
99
+ Console.instance.indent
100
+ # run the request
101
+ options[:ssl_verifypeer] = false
102
+ options[:followlocation] = true
103
+
104
+ Injector.decorate(options)
105
+
106
+ # convert all headers keys to strings to avoid having symbols like :"header" when
107
+ # declaring headers with colons instead of arrows
108
+ if options.key?(:headers)
109
+ new_opts = {}
110
+ options[:headers].map do |k, v|
111
+ new_opts[k.to_s] = v
112
+ end
113
+ options[:headers] = new_opts
114
+ end
115
+
116
+ if options.key?(:headers) and options[:headers].key?('Content-Type')
117
+ ctype = options[:headers]['Content-Type']
118
+ if ctype.include?('application/json')
119
+ # automatically encode json content
120
+ options[:body] = JSON.generate(options[:body], quirks_mode: true)
121
+ end
122
+ end
123
+
124
+
125
+
126
+ self.response = Typhoeus::Request.new(url, options).run
127
+
128
+ self.req_response = RequestResponse.new.tap { |r|
129
+ r.raw_body = response.body
130
+ r.headers = response.headers
131
+ r.code = response.code
132
+ r.total_time = response.total_time
133
+
134
+ if !r.headers.nil? && r.headers.key?('Content-Type') && r.headers['Content-Type'].include?('application/json')
135
+ r.body = JSON.parse(response.body)
136
+ else
137
+ r.body = response.body
138
+ end
139
+ }
140
+
141
+ # reset assertion counter
142
+ self.assert_no = 1
143
+
144
+ # evaluate response against expectations
145
+ begin
146
+ instance_eval(&expectations)
147
+ rescue AssertionException
148
+ error error_msg + " at #{expectations.source_location}"
149
+ raise RequestException
150
+ rescue StandardError => e
151
+ error 'Exception ' + e.message
152
+ info e.backtrace.inspect
153
+ _debug_info
154
+ error error_msg
155
+ raise RequestException
156
+ ensure
157
+ Console.instance.unindent
158
+ end
159
+
160
+ req_response
161
+
162
+ end
163
+
164
+ def error_msg
165
+ "REQUEST '#{options[:method].upcase} #{url}' failed"
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,48 @@
1
+ require 'greenlight/console'
2
+ require 'greenlight/request'
3
+ require 'greenlight/test'
4
+
5
+ module Greenlight
6
+
7
+ class ScenarioException < StandardError; end
8
+
9
+ class Scenario
10
+
11
+ # tests - hash of tests
12
+ attr_accessor :name, :body
13
+
14
+ def initialize(name, &block)
15
+ self.name = name
16
+ self.body = block
17
+ end
18
+
19
+ def run
20
+ action Colors.white("SCENARIO ") + Colors.light_magenta(name)
21
+ Console.instance.indent
22
+ ret = Greenlight.eval({}, &body)
23
+ success('scenario succeeded')
24
+ ret
25
+ rescue RequestException
26
+ error error_msg
27
+ raise ScenarioException
28
+ rescue TestException
29
+ error error_msg
30
+ raise ScenarioException
31
+ rescue LibraryException
32
+ error error_msg
33
+ raise ScenarioException
34
+ rescue StandardError => e
35
+ error e.backtrace.inspect
36
+ error e.message
37
+ error error_msg
38
+ raise ScenarioException
39
+ ensure
40
+ Console.instance.unindent
41
+ end
42
+
43
+ def error_msg
44
+ "SCENARIO '#{name}' failed"
45
+ end
46
+ end
47
+
48
+ end
@@ -0,0 +1,45 @@
1
+ require 'greenlight/console'
2
+ require 'greenlight/request'
3
+ require 'greenlight/library'
4
+
5
+ module Greenlight
6
+ class TestException < StandardError; end
7
+
8
+ class Test
9
+ attr_accessor :name
10
+ attr_accessor :body
11
+ attr_accessor :args
12
+
13
+ def initialize(name, &block)
14
+ self.name = name
15
+ self.body = block
16
+ end
17
+
18
+ def run(args = {})
19
+ action Colors.white("TEST ") + Colors.yellow(name)
20
+ Console.instance.indent
21
+ begin
22
+ ret = Greenlight.eval(args, &body)
23
+ success('test succeeded')
24
+ ret
25
+ rescue RequestException
26
+ error error_msg
27
+ raise TestException
28
+ rescue LibraryException
29
+ error error_msg
30
+ raise TestException
31
+ rescue StandardError => e
32
+ error e.backtrace.inspect
33
+ error e.message
34
+ error error_msg
35
+ raise TestException
36
+ ensure
37
+ Console.instance.unindent
38
+ end
39
+ end
40
+
41
+ def error_msg
42
+ "TEST '#{name}' failed"
43
+ end
44
+ end
45
+ end
data/lib/greenlight.rb ADDED
@@ -0,0 +1,164 @@
1
+ require 'greenlight/console'
2
+ require 'greenlight/request'
3
+ require 'greenlight/test'
4
+ require 'greenlight/scenario'
5
+ require 'singleton'
6
+ require 'yaml'
7
+ require 'json'
8
+ require 'typhoeus'
9
+
10
+ module Greenlight
11
+ class Runner
12
+ include Singleton
13
+
14
+ attr_accessor :params, :data
15
+
16
+ ATLAS_ENV_PREFIX = 'greenlight'
17
+ ATLAS_ENV_SEPARATOR = '_'
18
+
19
+ private
20
+ def load_data_url(uri)
21
+ resp = Typhoeus::Request.new(uri, {}).run
22
+ if resp.code == 200
23
+ case resp.headers['Content-Type'].split(';')[0]
24
+ when 'application/json'
25
+ JSON.parse(resp.body)
26
+ when 'text/yaml'
27
+ YAML.load(resp.body)
28
+ else
29
+ error 'unsupported data file format; only json and yaml are supported'
30
+ error resp.headers['Content-Type'] + ' found'
31
+ failure
32
+ end
33
+
34
+ else
35
+ error "loading test data from #{uri} failed with code #{resp.code}"
36
+ failure
37
+ end
38
+
39
+ end
40
+
41
+ def load_data_file(uri)
42
+ ext = File.extname(uri)
43
+ case ext
44
+ when '.json'
45
+ JSON.parse(File.read(uri))
46
+ when '.yml'
47
+ YAML.load_file(uri)
48
+ else
49
+ error 'unsupported file format: ' + ext
50
+ error 'only .json and .yml are supported'
51
+ end
52
+ end
53
+
54
+ def env_override(env_var)
55
+ ptr = data
56
+ parts = env_var.split(ATLAS_ENV_SEPARATOR)
57
+ parts[1, parts.length - 2].each do |p|
58
+ if ptr[p].is_a? Hash
59
+ ptr = ptr[p]
60
+ else
61
+ ptr[p] = {}
62
+ ptr = ptr[p]
63
+ end
64
+ end
65
+
66
+ ptr[parts[parts.length - 1]] = ENV[env_var]
67
+ end
68
+
69
+ def load_env
70
+ ENV.keys.each do |k|
71
+
72
+ next unless k.start_with?(ATLAS_ENV_PREFIX + ATLAS_ENV_SEPARATOR)
73
+ env_override(k)
74
+
75
+ end
76
+ end
77
+
78
+ public
79
+
80
+ %w[post get options delete put patch].each do |method|
81
+ define_method(method.to_s) do |url, options = {}|
82
+ options[:url] = url
83
+ options[:method] = method.to_sym
84
+ req = Request.new url, options
85
+ req
86
+ end
87
+ end
88
+
89
+ def initialize
90
+ self.params = {}
91
+ self.data = {}
92
+ end
93
+
94
+ def load_data(uri, overwrite = false)
95
+ info "loading data from #{uri}"
96
+ if uri.start_with?('http://', 'https://')
97
+ new_data = load_data_url(uri)
98
+ else
99
+ new_data = load_data_file(uri)
100
+ end
101
+
102
+ if overwrite
103
+ self.data = new_data
104
+ else
105
+ self.data.merge!(new_data)
106
+ end
107
+
108
+ load_env
109
+ new_data
110
+ end
111
+
112
+ def test(name, &block)
113
+ Test.new(name, &block).run
114
+ end
115
+
116
+ def scenario(name, &block)
117
+ Scenario.new(name, &block).run
118
+ end
119
+
120
+ def add_header(header, value)
121
+ Injector.instance.add_header(header, value)
122
+ end
123
+
124
+ def rm_header(header)
125
+ Injector.instance.rm_header(header)
126
+ end
127
+
128
+ def add_headers(headers)
129
+ headers.each do |key, val|
130
+ Injector.instance.add_header(key, val)
131
+ end
132
+ end
133
+
134
+ end
135
+
136
+ def self.eval(params, &block)
137
+ runner = Runner.instance
138
+ runner.params = params
139
+ runner.instance_eval(&block)
140
+ end
141
+ end
142
+
143
+ def greenlight(&block)
144
+ begin
145
+ Greenlight.eval({}, &block)
146
+ rescue Greenlight::LibraryException
147
+ failure
148
+ rescue Greenlight::ScenarioException
149
+ failure
150
+ rescue Greenlight::TestException
151
+ failure
152
+ rescue Greenlight::RequestException
153
+ failure
154
+ rescue StandardError => e
155
+ info e.backtrace.inspect
156
+ error e.message
157
+ failure
158
+ end
159
+ end
160
+
161
+ def failure
162
+ error 'TEST RUN FAILED'
163
+ abort
164
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: greenlight
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.pre.alpha
5
+ platform: ruby
6
+ authors:
7
+ - Florin Mihalache
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-04-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: typhoeus
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ description: A Ruby DSL to help writing automated tests for APIs.
28
+ email: florin.mihalache@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/greenlight.rb
34
+ - lib/greenlight/console.rb
35
+ - lib/greenlight/injector.rb
36
+ - lib/greenlight/request.rb
37
+ - lib/greenlight/scenario.rb
38
+ - lib/greenlight/test.rb
39
+ homepage: http://github.com/mflorin/greenlight
40
+ licenses:
41
+ - MIT
42
+ metadata: {}
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">"
55
+ - !ruby/object:Gem::Version
56
+ version: 1.3.1
57
+ requirements: []
58
+ rubyforge_project:
59
+ rubygems_version: 2.5.2.3
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: Automated Tests Language for APIs
63
+ test_files: []