specimen 0.0.2.alpha → 0.0.4.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +53 -49
  3. data/VERSION +1 -1
  4. data/lib/specimen/command/exec_command_builder.rb +42 -14
  5. data/lib/specimen/command/runner/cukes_runner.rb +2 -18
  6. data/lib/specimen/command/runner/specs_runner.rb +2 -17
  7. data/lib/specimen/command/test_runner.rb +19 -25
  8. data/lib/specimen/command.rb +12 -7
  9. data/lib/specimen/commands/encrypted_configuration/USAGE +37 -0
  10. data/lib/specimen/commands/encrypted_configuration/encrypted_configuration_command.rb +108 -0
  11. data/lib/specimen/commands/gem_help/USAGE +6 -3
  12. data/lib/specimen/config_parser.rb +31 -0
  13. data/lib/specimen/generator/configs/specimen_project_config.rb +2 -2
  14. data/lib/specimen/generator/cucumber/cucumber_project_generator.rb +2 -0
  15. data/lib/specimen/generator/cucumber/templates/config/cucumber.yml.tt +4 -1
  16. data/lib/specimen/generator/cucumber/templates/config/specimen.cukes.yml.tt +22 -0
  17. data/lib/specimen/generator/cucumber/templates/features/examples/add_numbers.feature.tt +4 -2
  18. data/lib/specimen/generator/cucumber/templates/features/step_definitions/examples/example_steps.rb.tt +9 -0
  19. data/lib/specimen/generator/cucumber/templates/features/support/env.rb.tt +22 -0
  20. data/lib/specimen/generator/project/project_root_generator.rb +0 -3
  21. data/lib/specimen/generator/project/specimen_project_generator.rb +37 -0
  22. data/lib/specimen/generator/project/templates/root/config/specimen.yml.tt +11 -7
  23. data/lib/specimen/generator/rspec/rspec_project_generator.rb +1 -0
  24. data/lib/specimen/generator/rspec/templates/config/.rspec.tt +0 -4
  25. data/lib/specimen/generator/rspec/templates/config/specimen.specs.yml.tt +19 -0
  26. data/lib/specimen/generator/rspec/templates/spec/examples/example_spec.rb.tt +9 -5
  27. data/lib/specimen/generator/rspec/templates/spec/spec_helper.rb.tt +7 -5
  28. data/lib/specimen/runtime.rb +124 -54
  29. data/lib/specimen/utils/encrypted_config_path.rb +54 -0
  30. data/lib/specimen/utils/encrypted_configuration.rb +156 -0
  31. data/lib/specimen/utils.rb +8 -0
  32. data/lib/specimen/version.rb +1 -1
  33. data/lib/specimen.rb +14 -5
  34. metadata +26 -6
  35. data/lib/specimen/command/runner/exec_runner.rb +0 -37
  36. data/lib/specimen/commands/exec/exec_command.rb +0 -9
  37. data/lib/specimen/runtime/yml_parser.rb +0 -49
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Given('I add {int} and {int}') do |int, int2|
4
+ @result = int + int2
5
+ end
6
+
7
+ Then('the result should be {int}') do |int|
8
+ expect(@result).to eq int
9
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib_path = "#{File.expand_path(__dir__)}/lib"
4
+ $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
5
+
6
+ require 'pry' if ENV.key?('DEBUG')
7
+ require 'specimen'
8
+ require 'rspec'
9
+
10
+ World RSpec::Expectations, RSpec::Matchers
11
+
12
+ BeforeAll do
13
+ Specimen.run_testrunner_hooks!
14
+ end
15
+
16
+ Before do
17
+ @enc_config = Specimen.enc_config
18
+ end
19
+
20
+ at_exit do
21
+ p 'bye bye'
22
+ end
@@ -11,11 +11,8 @@ module Specimen
11
11
  .rubocop.yml
12
12
  Gemfile
13
13
  README.md
14
- config/specimen.yml
15
14
  ].freeze
16
15
 
17
- # argument :config
18
-
19
16
  def execute!
20
17
  perform
21
18
  end
@@ -13,12 +13,49 @@ module Specimen
13
13
  CucumberProjectGenerator.start([config]) if config[:cucumber]
14
14
  RSpecProjectGenerator.start([config]) if config[:rspec]
15
15
 
16
+ inside config[:root_path] do
17
+ run('specimen enc create --name example')
18
+
19
+ env_file = '.example.env'
20
+ enc_key = File.read "#{Dir.pwd}/config/enc/example.key"
21
+ content = "MASTER_KEY='#{enc_key}'\n"
22
+ File.write(env_file, content)
23
+
24
+ say("Created env-file '#{env_file}' containing the MASTER_KEY to decrypt config/enc/example.yml.enc".bold)
25
+ end
26
+
27
+ say(init_message.green.bold)
16
28
  true
17
29
  end
18
30
 
19
31
  def config
20
32
  @config ||= Generator::SpecimenProjectConfig.parse(options)
21
33
  end
34
+
35
+ def init_message
36
+ enc_config = 'config/enc/example.yml.enc'
37
+
38
+ <<~STRING
39
+
40
+ Created new specimen project in
41
+ #{config[:root_path]}
42
+
43
+ Please cd into the directory and run e.g.
44
+
45
+ # check out the help for cukes and specs command
46
+ $> specimen cukes|specs --help|-h
47
+
48
+ # run tests using the encrypted example configuration
49
+ $> specimen cukes|specs --specimen-profile|--sp examples
50
+
51
+ # Check out the 'enc' command help
52
+ $> specimen enc --help|-h
53
+
54
+ # Read and update the encrypted config '#{enc_config}'
55
+ $> specimen enc update --name|-n example
56
+
57
+ STRING
58
+ end
22
59
  end
23
60
  end
24
61
  end
@@ -3,6 +3,7 @@ default: &default
3
3
  env:
4
4
  - FOO='123'
5
5
 
6
+ <% if data[:cucumber] -%>
6
7
  cucumber: &cucumber
7
8
  framework: cucumber
8
9
  profiles: []
@@ -14,6 +15,15 @@ cucumber: &cucumber
14
15
  - FOO='123'
15
16
  - BAZ='something'
16
17
 
18
+ ci_cukes:
19
+ <<: *cucumber
20
+ env:
21
+ - CI='1'
22
+ profiles:
23
+ - regression
24
+ <% end -%>
25
+
26
+ <% if data[:rspec] -%>
17
27
  rspec: &rspec
18
28
  framework: rspec
19
29
  options:
@@ -29,10 +39,4 @@ ci_specs:
29
39
  <<: *rspec
30
40
  env:
31
41
  - CI='1'
32
-
33
- ci_cukes:
34
- <<: *cucumber
35
- env:
36
- - CI='1'
37
- profiles:
38
- - regression
42
+ <% end -%>
@@ -9,6 +9,7 @@ module Specimen
9
9
  spec/examples/example_spec.rb
10
10
  spec/spec_helper.rb
11
11
  config/.rspec
12
+ config/specimen.specs.yml
12
13
  ].freeze
13
14
 
14
15
  def execute!
@@ -1,5 +1 @@
1
1
  --require ./spec/spec_helper
2
-
3
- --format progress
4
- --format documentation
5
- --format html --out tmp/rspec_result.html
@@ -0,0 +1,19 @@
1
+ default_opts: &default_opts
2
+ - --options config/.rspec
3
+ - --format documentation
4
+ - --format html --out tmp/rspec_result.html
5
+
6
+ rspec: &rspec
7
+ options: *default_opts
8
+
9
+ examples:
10
+ <<: *rspec
11
+ env_file: .example.env
12
+ enc_configs:
13
+ - name: example
14
+ env_key: MASTER_KEY
15
+
16
+ ci_specs:
17
+ <<: *rspec
18
+ env:
19
+ - CI='1'
@@ -1,13 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- RSpec.describe 'Example specs' do
4
- context 'run example tests' do
3
+ RSpec.describe 'Add numbers specs' do
4
+ context 'add 1 + 1', add_numbers: true do
5
5
  before(:example) do
6
- @foo = '123'
6
+ @result = 1 + 1
7
7
  end
8
8
 
9
- it 'passes' do
10
- expect(@foo).to eq '123'
9
+ it 'passes', pass: true do
10
+ expect(@result).to eq 2
11
+ end
12
+
13
+ it 'fails due to not being an integer', fail: true do
14
+ expect(@result).to eq '2'
11
15
  end
12
16
  end
13
17
  end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # require rubygems
4
- require 'pry'
3
+ require 'specimen'
5
4
 
6
5
  RSpec.configure do |config|
7
- # generic base config for each example
8
- config.before(:example) do
9
- p 'always running'
6
+ config.before(:suite) do
7
+ Specimen.run_testrunner_hooks!
8
+ end
9
+
10
+ config.before(:all) do
11
+ @enc_config = Specimen.enc_config
10
12
  end
11
13
  end
12
14
 
@@ -1,84 +1,154 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'runtime/yml_parser'
3
+ require 'dotenv'
4
+ require 'specimen/config_parser'
4
5
 
5
6
  module Specimen
6
- module Runtime
7
- class MissingCommandOptionError < RuntimeError
8
- def initialize(command, option)
9
- @msg = "Command '#{command}' misses option: '#{option}'"
10
- super(@msg)
11
- end
7
+ class Runtime
8
+ class ConfigNotFoundError < RuntimeError; end
9
+ class ProfileNotFoundError < RuntimeError; end
10
+
11
+ attr_reader :wd_path, :config_directory,
12
+ :program_name, :config_data,
13
+ :profile_data, :framework
14
+
15
+ attr_accessor :command, :specimen_config, :specimen_profile
16
+
17
+ def initialize
18
+ @wd_path = Pathname.getwd
19
+ @config_directory = Pathname.new("#{wd_path}/config")
20
+ @program_name = $PROGRAM_NAME
21
+ @command = nil
22
+ @specimen_config = nil
23
+ @specimen_profile = nil
24
+ @config_data = nil
25
+ @profile_data = nil
12
26
  end
13
27
 
14
- class UndefinedYmlError < RuntimeError
15
- def initialize
16
- @msg = "Option '--config-fie' is not set!"
17
- super(@msg)
18
- end
28
+ def set_testrunner!(config, profile, command)
29
+ @specimen_config = config
30
+ @specimen_profile = profile
31
+ @command = command
32
+
33
+ define_framework!
34
+ run_default_profile_checks!
35
+
36
+ load_specimen_config!
37
+ load_specimen_profile!
19
38
  end
20
39
 
21
- class YmlNotFoundError < RuntimeError
22
- def initialize(yml_path)
23
- @msg = "No such file: '#{yml_path.to_path}'"
24
- super(@msg)
25
- end
40
+ def run_load_profile_hook!
41
+ @specimen_config = ENV.fetch('SPECIMEN_CONFIG_NAME')
42
+ @specimen_profile = ENV.fetch('SPECIMEN_PROFILE_NAME')
43
+
44
+ load_specimen_config!
45
+ load_specimen_profile!
26
46
  end
27
47
 
28
- DEFAULT_YML_NAME = 'specimen.yml'
48
+ def run_env_file_hook!
49
+ return unless profile_with_env_file?
29
50
 
30
- class << self
31
- attr_reader :command, :config
51
+ file = profile_data['env_file']
52
+ path = Pathname.new("#{wd_path}/#{file}")
32
53
 
33
- def start!(command, **config)
34
- @command = command
35
- @config = config
54
+ return Dotenv.load!(path.to_path) if path.exist?
36
55
 
37
- return if skip_yml_load?
38
- raise YmlNotFoundError, yml_path unless yml_path.exist?
56
+ raise "Environment file: '#{path.to_path}' is defined in profile '#{specimen_profile}' but it does not exist!"
57
+ end
39
58
 
40
- self
41
- end
59
+ def run_decrypt_enc_configs_hook!
60
+ return unless profile_with_enc_configs?
42
61
 
43
- def work_dir
44
- @work_dir ||= Pathname.getwd
45
- end
62
+ encrypted_config = {}
63
+ enc_configs = profile_data['enc_configs']
46
64
 
47
- def config_dir
48
- @config_dir ||= Pathname.new("#{work_dir}/config")
49
- end
65
+ enc_configs.each do |enc_config|
66
+ name = enc_config['name']
67
+ env_key = enc_config['env_key']
50
68
 
51
- def yml_path
52
- @yml_path ||= Pathname.new("#{config_dir}/#{yml_name}")
53
- end
69
+ warn "env key '#{env_key}' defined but not set!" if env_key && !ENV.key?(env_key)
70
+ env_key.nil? ? env_key = '' : env_key
54
71
 
55
- def skip_yml_load?
56
- command.is_a?(Command::InitCommand)
72
+ config = Specimen::Utils::EncryptedConfiguration.decrypt(name:, env_key:)
73
+ encrypted_config.merge!(config)
57
74
  end
58
75
 
59
- def yml_name
60
- return @yml_name if @yml_name
76
+ Specimen.enc_config = encrypted_config
77
+ end
61
78
 
62
- key_name = 'config_file'
63
- raise MissingCommandOptionError, command, key_name unless command.options.key?(key_name)
79
+ def define_framework!
80
+ raise 'Unrecognized command' unless cukes? || specs?
64
81
 
65
- yml_name = command.options[key_name]
66
- raise UndefinedYmlError if yml_name.empty?
82
+ @framework = cukes? ? 'cucumber' : 'rspec'
83
+ end
67
84
 
68
- @yml_name = yml_name
69
- end
85
+ def run_default_profile_checks!
86
+ raise 'You can not use the rspec profile with Cucumber!' if cukes? && specimen_profile == 'rspec'
87
+ raise 'You can not use the cucumber profile with RSpec!' if specs? && specimen_profile == 'cucumber'
88
+ end
70
89
 
71
- def yml_data
72
- @yml_data ||= YmlParser.parse!(yml_path.to_path)
73
- end
90
+ def load_specimen_profile!
91
+ raise 'Specimen profile is not set!' if specimen_profile.nil? || specimen_profile.empty?
74
92
 
75
- def default_yml_data
76
- @default_yml_data ||= profile_yml_data('default')
93
+ unless config_data&.key?(specimen_profile)
94
+ raise ProfileNotFoundError, "Can not find profile '#{specimen_profile}' in #{specimen_config_path.to_path}"
77
95
  end
78
96
 
79
- def profile_yml_data(profile = nil)
80
- yml_data[profile]
81
- end
97
+ @profile_data = config_data[specimen_profile]
98
+ end
99
+
100
+ def load_specimen_config!
101
+ raise ConfigNotFoundError, 'Specimen config not found' unless specimen_config_exist?
102
+
103
+ @config_data = ConfigParser.read!(specimen_config_path.to_path)
104
+ end
105
+
106
+ def specimen_config_path
107
+ Pathname.new("#{config_directory}/#{specimen_config}")
108
+ end
109
+
110
+ def custom_config
111
+ @specimen_config
112
+ end
113
+
114
+ def profile_with_enc_configs?
115
+ return false unless profile_data.is_a?(Hash)
116
+
117
+ profile_data&.key?('enc_configs')
118
+ end
119
+
120
+ def profile_with_env_file?
121
+ return false unless profile_data.is_a?(Hash)
122
+
123
+ profile_data&.key?('env_file')
124
+ end
125
+
126
+ def cukes?
127
+ return false if command.nil?
128
+
129
+ command.is_a?(Command::CukesCommand)
130
+ end
131
+
132
+ def specs?
133
+ return false if command.nil?
134
+
135
+ command.is_a?(Command::SpecsCommand)
136
+ end
137
+
138
+ def specimen_config_exist?
139
+ specimen_config_path.exist?
140
+ end
141
+
142
+ def specimen_bin?
143
+ program_name.include?('bin/specimen')
144
+ end
145
+
146
+ def cucumber_bin?
147
+ program_name.include?('bin/cucumber')
148
+ end
149
+
150
+ def parallel_cucumber_bin?
151
+ program_name.include?('bin/parallel_cucumber')
82
152
  end
83
153
  end
84
154
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Specimen
4
+ module Utils
5
+ class EncryptedConfigPath
6
+ ENC_DIRECTORY = 'config/enc'
7
+
8
+ attr_reader :name, :runtime
9
+
10
+ def initialize(name:)
11
+ @name = name
12
+ @runtime = Specimen.runtime
13
+ end
14
+
15
+ def config_base_dir
16
+ @config_base_dir ||= Pathname.new("#{runtime.wd_path}/#{ENC_DIRECTORY}")
17
+ end
18
+
19
+ def enc_dir
20
+ return Pathname.new(config_base_dir.to_path) if config_dir.empty?
21
+
22
+ Pathname.new("#{config_base_dir}/#{config_dir}")
23
+ end
24
+
25
+ def config_file_name
26
+ "#{split_name.last}.yml.enc"
27
+ end
28
+
29
+ def config_dir
30
+ split_name[0...-1].join('/')
31
+ end
32
+
33
+ def split_name
34
+ name.split('/')
35
+ end
36
+
37
+ def full_enc_path
38
+ @full_enc_path ||= Pathname.new("#{enc_dir}/#{config_file_name}")
39
+ end
40
+
41
+ def full_key_path
42
+ @full_key_path ||= Pathname.new("#{enc_dir}/#{config_file_name.gsub('.yml.enc', '.key')}")
43
+ end
44
+
45
+ def config_exist?
46
+ full_enc_path.exist?
47
+ end
48
+
49
+ def key_exist?
50
+ full_key_path.exist?
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/encrypted_configuration'
4
+ require 'specimen/utils/encrypted_config_path'
5
+
6
+ module Specimen
7
+ module Utils
8
+ class EncryptedConfiguration
9
+ class ExistingConfigFilesError < StandardError; end
10
+ class NoSuchConfigError < StandardError; end
11
+ class MissingKeyFileError < StandardError; end
12
+ class MissingKeyFileError < StandardError; end
13
+
14
+ attr_accessor :name
15
+ attr_reader :enc_path, :env_key, :config_path, :key_path
16
+
17
+ def self.create(name:)
18
+ new(name:).create_encrypted_config!
19
+ end
20
+
21
+ def self.update(name:)
22
+ new(name:).update_encrypted_config!
23
+ end
24
+
25
+ def self.validate(name:)
26
+ new(name:).validate_encrypted_config!
27
+ end
28
+
29
+ def self.decrypt(name:, env_key: 'MASTER_KEY')
30
+ new(name:, env_key:).decrypt_config!
31
+ end
32
+
33
+ def initialize(name:, env_key: '')
34
+ @name = name
35
+ @enc_path = EncryptedConfigPath.new(name:)
36
+ @config_path = @enc_path.full_enc_path
37
+ @key_path = @enc_path.full_key_path
38
+ @env_key = env_key
39
+ end
40
+
41
+ def decrypt_config!
42
+ raise NoSuchConfigError, "No such file: '#{config_path.to_path}'" unless config_exist?
43
+ raise "Missing decryption key for enc-config '#{enc_path.name}'" unless decryption_key?
44
+
45
+ YAML.load(enc_yml_content).deep_symbolize_keys!
46
+ end
47
+
48
+ def validate_encrypted_config!
49
+ run_enc_files_check!
50
+ YAML.load(enc_yml_content)
51
+ enc_path
52
+ rescue StandardError => e
53
+ e
54
+ end
55
+
56
+ def enc_yml_content
57
+ enc_config.read
58
+ end
59
+
60
+ def decryption_key?
61
+ key_exist? || env_key_set?
62
+ end
63
+
64
+ def env_key_set?
65
+ return false if env_key.empty?
66
+
67
+ ENV.key?(env_key)
68
+ end
69
+
70
+ def run_enc_files_check!
71
+ raise NoSuchConfigError, "No such file: '#{config_path.to_path}'" unless config_exist?
72
+ raise MissingKeyFileError, "Missing encryption key file: '#{key_path.to_path}'" unless key_exist?
73
+ end
74
+
75
+ def update_encrypted_config!
76
+ raise NoSuchConfigError, "No such file: '#{config_path.to_path}'" unless config_exist?
77
+ raise MissingKeyFileError, "Missing encryption key file: '#{key_path.to_path}'" unless key_exist?
78
+ raise 'Missing EDITOR variable' unless editor_set?
79
+ raise "Can not find executable for editor '#{editor}'" unless editor?
80
+
81
+ enc_config.change do |tmp_path|
82
+ system("#{ENV.fetch('EDITOR')} #{tmp_path}")
83
+ end
84
+ end
85
+
86
+ def create_encrypted_config!
87
+ raise ExistingConfigFilesError, "Existing config at #{config_path.to_path}" if config_exist?
88
+ raise ExistingConfigFilesError, "Existing key file at #{key_path.to_path}" if key_exist?
89
+
90
+ create_key_file!
91
+ create_enc_config!
92
+
93
+ enc_path
94
+ end
95
+
96
+ def enc_config
97
+ @enc_config ||= ActiveSupport::EncryptedConfiguration.new(
98
+ config_path:,
99
+ key_path:,
100
+ env_key:,
101
+ raise_if_missing_key: true
102
+ )
103
+ end
104
+
105
+ def config_exist?
106
+ enc_path.config_exist?
107
+ end
108
+
109
+ def key_exist?
110
+ enc_path.key_exist?
111
+ end
112
+
113
+ def create_key_file!
114
+ FileUtils.mkdir_p(key_path.dirname) unless key_path.directory?
115
+ key_path.write(generate_key)
116
+ end
117
+
118
+ def create_enc_config!
119
+ enc_config.write(example_yml)
120
+ end
121
+
122
+ def generate_key
123
+ ActiveSupport::EncryptedFile.generate_key
124
+ end
125
+
126
+ def editor
127
+ ENV.fetch('EDITOR')
128
+ end
129
+
130
+ def editor_set?
131
+ ENV.fetch('EDITOR', false)
132
+ end
133
+
134
+ def editor?
135
+ system("command -v #{editor}")
136
+ end
137
+
138
+ def example_yml
139
+ <<~YML
140
+ user:
141
+ email: john.doe@example.com
142
+ password: johnspassword
143
+
144
+ service:
145
+ host: https://api.my-website.biz
146
+ client_id: secret-id
147
+ client_secret: client-secret
148
+
149
+ platform:
150
+ website_url: https://my-website.biz
151
+ admin_url: https://admin.my-website.biz
152
+ YML
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils/encrypted_configuration'
4
+
5
+ module Specimen
6
+ module Utils
7
+ end
8
+ end
@@ -8,7 +8,7 @@ module Specimen
8
8
  module VERSION
9
9
  MAJOR = 0
10
10
  MINOR = 0
11
- TINY = 2
11
+ TINY = 4
12
12
  PRE = 'alpha'
13
13
 
14
14
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
data/lib/specimen.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'active_support'
4
4
  require 'colorize'
5
5
  require 'pathname'
6
+ require 'psych'
6
7
 
7
8
  # require Ruby extensions
8
9
  require 'specimen/extensions/ruby/hash'
@@ -12,13 +13,21 @@ require 'specimen/runtime'
12
13
  require 'specimen/version'
13
14
 
14
15
  module Specimen
15
- extend ActiveSupport::Autoload
16
-
17
16
  class << self
18
- attr_accessor :runtime
17
+ attr_accessor :enc_config
18
+
19
+ def runtime
20
+ @runtime ||= Specimen::Runtime.new
21
+ end
22
+
23
+ def run_testrunner_hooks!
24
+ runtime.run_load_profile_hook!
25
+ runtime.run_env_file_hook!
26
+ runtime.run_decrypt_enc_configs_hook!
27
+ end
19
28
 
20
- def init_wd_path
21
- @init_wd_path ||= Pathname.getwd
29
+ def shell
30
+ runtime&.command.shell
22
31
  end
23
32
  end
24
33
  end