specimen 0.0.3.alpha → 0.0.4.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.
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 -12
  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 -5
  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 +3 -3
  12. data/lib/specimen/config_parser.rb +31 -0
  13. data/lib/specimen/generator/configs/specimen_project_config.rb +1 -1
  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 -1
  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 +13 -5
  34. metadata +11 -5
  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 -47
@@ -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,7 +11,6 @@ module Specimen
11
11
  .rubocop.yml
12
12
  Gemfile
13
13
  README.md
14
- config/specimen.yml
15
14
  ].freeze
16
15
 
17
16
  def execute!
@@ -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 = 3
11
+ TINY = 4
12
12
  PRE = 'alpha'
13
13
 
14
14
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
data/lib/specimen.rb CHANGED
@@ -13,13 +13,21 @@ require 'specimen/runtime'
13
13
  require 'specimen/version'
14
14
 
15
15
  module Specimen
16
- extend ActiveSupport::Autoload
17
-
18
16
  class << self
19
- 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
20
28
 
21
- def init_wd_path
22
- @init_wd_path ||= Pathname.getwd
29
+ def shell
30
+ runtime&.command.shell
23
31
  end
24
32
  end
25
33
  end