secret_config 0.6.4 → 0.7.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a86408c0a69f9e1c3e67a06d004c659abe13f7b482a3ff11e8de6b99c650d05
4
- data.tar.gz: cd16668968bf171809b47b2c3b749de2ef63a7fb91245cdf68bac253aeb5639b
3
+ metadata.gz: 24792842f7d81af2c5d70eb431eae87ec96f190a52500c7e68267b6e875a4e09
4
+ data.tar.gz: 29f1683c2be73597476f976860dd021476559d97ed94e04195789fadaf4fb7f8
5
5
  SHA512:
6
- metadata.gz: b71279e1a7855b8e081239f3eea1bf05c3c6edc5e4226e8e31e347667480156c493f87443d09bc060a2829a7cd87ab1acdcba9e0cad9811c634355b4944ecea7
7
- data.tar.gz: ce127ef03fa642011deb394e3f3cd3de94e8d4ace3de712dbc218dd080ed4ea03ce2ff06c8ee35d32e2b06765e08c93a92cf2066ca072e4d16380e52d2d58504
6
+ metadata.gz: 89a0ecfab4bfb8263768ccf51ec56b987866cac08d23c2c9e34a69b9ba3210d9ed40198cfd525d72f54dcf4e6aa0f2c8965d30204ec96c4b825f1c61313c35b5
7
+ data.tar.gz: c0047de74541f0589ebe2ad66a67133b73d6fa5635610d81b685394aca395d46dfadc4171ca655975296289292203dc004a977fe265c6b0d704ad4a894cc6983
data/README.md CHANGED
@@ -5,6 +5,13 @@ Centralized Configuration and Secrets Management for Ruby and Rails applications
5
5
 
6
6
  Securely store configuration information centrally, supporting multiple tenants of the same application.
7
7
 
8
+ ## Overview
9
+
10
+ Securely store centralized configuration information such as:
11
+ * Settings
12
+ * Passwords
13
+ * Encryption keys and certificates
14
+
8
15
  ## Features
9
16
 
10
17
  Supports storing configuration information in:
@@ -15,6 +22,17 @@ Supports storing configuration information in:
15
22
  * AWS System Manager Parameter Store
16
23
  * Encrypt and securely store secrets such as passwords centrally.
17
24
 
25
+ Supported data types:
26
+ * integer
27
+ * float
28
+ * string
29
+ * boolean
30
+ * symbol
31
+ * json
32
+
33
+ Supported conversions:
34
+ * base64
35
+
18
36
  ## Benefits
19
37
 
20
38
  Benefits of moving sensitive configuration information into AWS System Manager Parameter Store:
@@ -23,7 +41,7 @@ Benefits of moving sensitive configuration information into AWS System Manager P
23
41
  * Environment variables force all config into a single level.
24
42
  * Reduces the number of environment variables.
25
43
  * In a large application the number of secrets can grow dramatically.
26
- * Removes the need to encrypt sensitive data config files.
44
+ * Replaces sensitive data stored in local yaml or configuration files.
27
45
  * Including securing and managing encryption keys.
28
46
  * When encryption keys change, such as during a key rotation, config files don't have to be changed.
29
47
  * Removes security concerns with placing passwords in the clear into environment variables.
@@ -222,7 +240,7 @@ Note: Do not put any production credentials into this file.
222
240
 
223
241
  ### Environment Variables
224
242
 
225
- Any of the above values can be overridden with an environment variable.
243
+ Any of the above values can be overridden with an environment variable, unless explicitly configured `SecretConfig.check_env_var = false`.
226
244
 
227
245
  To overwrite any of these settings with an environment variable:
228
246
 
@@ -471,6 +489,49 @@ to view and modify parameters:
471
489
  - `ssm:GetParameters`
472
490
  - `ssm:GetParameter`
473
491
 
492
+ ## String Interpolation
493
+
494
+ Values supplied for config settings can be replaced inline with date, time, hostname, pid and random values.
495
+
496
+ For example to include the `hostname` in the log file name setting:
497
+
498
+ ~~~yaml
499
+ development:
500
+ logger:
501
+ level: info
502
+ file_name: /var/log/my_application_%{hostname}.log
503
+ ~~~
504
+
505
+ Available interpolations:
506
+
507
+ * %{date}
508
+ * Current date in the format of "%Y%m%d" (CCYYMMDD)
509
+ * %{date:format}
510
+ * Current date in the supplied format. See strftime
511
+ * %{time}
512
+ * Current date and time down to ms in the format of "%Y%m%d%Y%H%M%S%L" (CCYYMMDDHHMMSSmmm)
513
+ * %{time:format}
514
+ * Current date and time in the supplied format. See strftime
515
+ * %{env:name}
516
+ * Extract value from the named environment variable.
517
+ * %{hostname}
518
+ * Full name of this host.
519
+ * %{hostname:short}
520
+ * Short name of this host. Everything up to the first period.
521
+ * %{pid}
522
+ * Process Id for this process.
523
+ * %{random}
524
+ * URL safe Random 32 byte value.
525
+ * %{random:size}
526
+ * URL safe Random value of `size` bytes.
527
+
528
+ #### Notes:
529
+
530
+ * To prevent interpolation use %%{...}
531
+ * %% is not touched, only %{...} is searched for.
532
+ * Since these interpolations are only evaluated at load time and
533
+ every time the registry is refreshed there is no runtime overhead when keys are fetched.
534
+
474
535
  ## Command Line Interface
475
536
 
476
537
  Secret Config has a command line interface for exporting, importing and copying between paths in the registry.
@@ -10,4 +10,7 @@ module SecretConfig
10
10
 
11
11
  class ConfigurationError < Error
12
12
  end
13
+
14
+ class InvalidInterpolation < Error
15
+ end
13
16
  end
@@ -62,7 +62,8 @@ module SecretConfig
62
62
  value: value.to_s,
63
63
  type: "SecureString",
64
64
  key_id: key_id,
65
- overwrite: true
65
+ overwrite: true,
66
+ tier: "Intelligent-Tiering"
66
67
  )
67
68
  end
68
69
 
@@ -32,7 +32,13 @@ module SecretConfig
32
32
 
33
33
  # Returns [String] configuration value for the supplied key, or nil when missing.
34
34
  def [](key)
35
- cache[expand_key(key)]
35
+ full_key = expand_key(key)
36
+ value = cache[full_key]
37
+ if value.nil? && SecretConfig.check_env_var?
38
+ value = env_var_override(key, value)
39
+ cache[full_key] = value unless value.nil?
40
+ end
41
+ value
36
42
  end
37
43
 
38
44
  # Returns [String] configuration value for the supplied key, or nil when missing.
@@ -45,8 +51,7 @@ module SecretConfig
45
51
  value = self[key]
46
52
  if value.nil?
47
53
  raise(MissingMandatoryKey, "Missing configuration value for #{path}/#{key}") if default == :no_default_supplied
48
-
49
- value = default.respond_to?(:call) ? default.call : default
54
+ value = block_given? ? yield : default
50
55
  end
51
56
 
52
57
  value = convert_encoding(encoding, value) if encoding
@@ -78,7 +83,8 @@ module SecretConfig
78
83
  existing_keys = cache.keys
79
84
  updated_keys = []
80
85
  provider.each(path) do |key, value|
81
- cache[key] = env_var_override(key, value)
86
+ value = interpolator.parse(value) if value.is_a?(String) && value.include?('%{')
87
+ cache[key] = env_var_override(relative_key(key), value)
82
88
  updated_keys << key
83
89
  end
84
90
 
@@ -92,10 +98,16 @@ module SecretConfig
92
98
 
93
99
  attr_reader :cache
94
100
 
101
+ def interpolator
102
+ @interpolator ||= SettingInterpolator.new
103
+ end
104
+
95
105
  # Returns the value from an env var if it is present,
96
106
  # Otherwise the value is returned unchanged.
97
107
  def env_var_override(key, value)
98
- env_var_name = relative_key(key).upcase.gsub("/", "_")
108
+ return value unless SecretConfig.check_env_var?
109
+
110
+ env_var_name = key.upcase.gsub("/", "_")
99
111
  ENV[env_var_name] || value
100
112
  end
101
113
 
@@ -0,0 +1,58 @@
1
+ require 'date'
2
+ require 'socket'
3
+ require 'securerandom'
4
+ # * SecretConfig Interpolations
5
+ #
6
+ # Expanding values inline for date, time, hostname, pid and random values.
7
+ # %{date} # Current date in the format of "%Y%m%d" (CCYYMMDD)
8
+ # %{date:format} # Current date in the supplied format. See strftime
9
+ # %{time} # Current date and time down to ms in the format of "%Y%m%d%Y%H%M%S%L" (CCYYMMDDHHMMSSmmm)
10
+ # %{time:format} # Current date and time in the supplied format. See strftime
11
+ # %{env:name} # Extract value from the named environment value.
12
+ # %{hostname} # Full name of this host.
13
+ # %{hostname:short} # Short name of this host. Everything up to the first period.
14
+ # %{pid} # Process Id for this process.
15
+ # %{random} # URL safe Random 32 byte value.
16
+ # %{random:size} # URL safe Random value of `size` bytes.
17
+ #
18
+ # Retrieve values elsewhere in the registry.
19
+ # Paths can be relative to the current root, or absolute paths outside the current root.
20
+ # %{fetch:key} # Fetches a single value from a relative or absolute path
21
+ # %{include:path} # Fetches a path of keys and values
22
+ module SecretConfig
23
+ class SettingInterpolator < StringInterpolator
24
+ def date(format = "%Y%m%d")
25
+ Date.today.strftime(format)
26
+ end
27
+
28
+ def time(format = "%Y%m%d%H%M%S%L")
29
+ Time.now.strftime(format)
30
+ end
31
+
32
+ def env(name)
33
+ ENV[name]
34
+ end
35
+
36
+ def hostname(format = nil)
37
+ name = Socket.gethostname
38
+ name = name.split('.')[0] if format == "short"
39
+ name
40
+ end
41
+
42
+ def pid
43
+ $$
44
+ end
45
+
46
+ def random(size = 32)
47
+ SecureRandom.urlsafe_base64(size)
48
+ end
49
+
50
+ #def fetch(key)
51
+ # SecretConfig[key]
52
+ #end
53
+ #
54
+ #def include(path)
55
+ #
56
+ #end
57
+ end
58
+ end
@@ -0,0 +1,32 @@
1
+ # Parse strings containing %{key:value1,value2,value3}
2
+ # Where `key` is a method implemented by a class inheriting from this class
3
+ #
4
+ # The following `key`s are reserved:
5
+ # * parse
6
+ # * initialize
7
+ #
8
+ # Notes:
9
+ # * To prevent interpolation use %%{...}
10
+ # * %% is not touched, only %{...} is identified.
11
+ module SecretConfig
12
+ class StringInterpolator
13
+ def initialize(pattern = nil)
14
+ @pattern = pattern || /%{1,2}\{([^}]+)\}/
15
+ end
16
+
17
+ def parse(string)
18
+ string.gsub(/%{1,2}\{([^}]+)\}/) do |match|
19
+ if match.start_with?('%%')
20
+ match[1..-1]
21
+ else
22
+ expr = $1 || $2 || match.tr("%{}", "")
23
+ key, args_str = expr.split(':')
24
+ key = key.strip.to_sym
25
+ arguments = args_str&.split(',')&.map { |v| v.strip == '' ? nil : v.strip } || []
26
+ raise(InvalidInterpolation, "Invalid key: #{key} in string: #{match}") unless respond_to?(key)
27
+ public_send(key, *arguments)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,3 +1,3 @@
1
1
  module SecretConfig
2
- VERSION = "0.6.4".freeze
2
+ VERSION = "0.7.0".freeze
3
3
  end
data/lib/secret_config.rb CHANGED
@@ -18,6 +18,8 @@ module SecretConfig
18
18
  end
19
19
 
20
20
  autoload :CLI, "secret_config/cli"
21
+ autoload :SettingInterpolator, "secret_config/setting_interpolator"
22
+ autoload :StringInterpolator, "secret_config/string_interpolator"
21
23
  autoload :Utils, "secret_config/utils"
22
24
 
23
25
  class << self
@@ -65,8 +67,6 @@ module SecretConfig
65
67
  @check_env_var = check_env_var
66
68
  end
67
69
 
68
- private
69
-
70
70
  @check_env_var = true
71
71
  @filters = [/password/i, /key\Z/i, /passphrase/i]
72
72
  end
@@ -41,7 +41,7 @@ test:
41
41
  mongo:
42
42
  database: secret_config_test
43
43
  primary: 127.0.0.1:27017
44
- secondary: 127.0.0.1:27018
44
+ secondary: "%{hostname}:27018"
45
45
 
46
46
  secrets:
47
47
  secret_key_base: somereallylongteststring
@@ -15,7 +15,7 @@ module Providers
15
15
  {
16
16
  "/test/my_application/mongo/database" => "secret_config_test",
17
17
  "/test/my_application/mongo/primary" => "127.0.0.1:27017",
18
- "/test/my_application/mongo/secondary" => "127.0.0.1:27018",
18
+ "/test/my_application/mongo/secondary" => "%{hostname}:27018",
19
19
  "/test/my_application/mysql/database" => "secret_config_test",
20
20
  "/test/my_application/mysql/password" => "secret_configrules",
21
21
  "/test/my_application/mysql/username" => "secret_config",
@@ -1,4 +1,5 @@
1
1
  require_relative 'test_helper'
2
+ require 'socket'
2
3
 
3
4
  class RegistryTest < Minitest::Test
4
5
  describe SecretConfig::Registry do
@@ -22,7 +23,7 @@ class RegistryTest < Minitest::Test
22
23
  {
23
24
  "/test/my_application/mongo/database" => "secret_config_test",
24
25
  "/test/my_application/mongo/primary" => "127.0.0.1:27017",
25
- "/test/my_application/mongo/secondary" => "127.0.0.1:27018",
26
+ "/test/my_application/mongo/secondary" => "#{Socket.gethostname}:27018",
26
27
  "/test/my_application/mysql/database" => "secret_config_test",
27
28
  "/test/my_application/mysql/password" => "secret_configrules",
28
29
  "/test/my_application/mysql/username" => "secret_config",
@@ -1,9 +1,10 @@
1
- require_relative 'test_helper'
1
+ require_relative "test_helper"
2
+ require "socket"
2
3
 
3
4
  class SecretConfigTest < Minitest::Test
4
5
  describe SecretConfig::Providers::File do
5
6
  let :file_name do
6
- File.join(File.dirname(__FILE__), 'config', 'application.yml')
7
+ File.join(File.dirname(__FILE__), "config", "application.yml")
7
8
  end
8
9
 
9
10
  let :path do
@@ -14,39 +15,56 @@ class SecretConfigTest < Minitest::Test
14
15
  SecretConfig.use :file, path: path, file_name: file_name
15
16
  end
16
17
 
17
- describe '#configuration' do
18
- it 'returns a copy of the config' do
18
+ describe "#configuration" do
19
+ it "returns a copy of the config" do
19
20
  assert_equal "127.0.0.1", SecretConfig.configuration.dig("mysql", "host")
20
21
  end
21
22
  end
22
23
 
23
- describe '#key?' do
24
- it 'has key' do
24
+ describe "#key?" do
25
+ it "has key" do
25
26
  assert SecretConfig.key?("mysql/database")
26
27
  end
27
28
  end
28
29
 
29
- describe '#[]' do
30
- it 'returns values' do
30
+ describe "#[]" do
31
+ it "returns values" do
31
32
  assert_equal "secret_config_test", SecretConfig["mysql/database"]
32
33
  end
34
+
35
+ it "returns values with interpolation" do
36
+ assert_equal "#{Socket.gethostname}:27018", SecretConfig["mongo/secondary"]
37
+ end
33
38
  end
34
39
 
35
- describe '#fetch' do
40
+ describe "#fetch" do
36
41
  after do
37
- ENV['MYSQL_DATABASE'] = nil
42
+ ENV["MYSQL_DATABASE"] = nil
43
+ SecretConfig.check_env_var = true
38
44
  end
39
45
 
40
- it 'fetches values' do
46
+ it "fetches values" do
41
47
  assert_equal "secret_config_test", SecretConfig.fetch("mysql/database")
42
48
  end
43
49
 
44
- it 'can be overridden by an environment variable' do
45
- ENV['MYSQL_DATABASE'] = 'other'
50
+ it "can be overridden by an environment variable" do
51
+ ENV["MYSQL_DATABASE"] = "other"
46
52
 
47
53
  SecretConfig.use :file, path: path, file_name: file_name
48
54
  assert_equal "other", SecretConfig.fetch("mysql/database")
49
55
  end
56
+
57
+ it "returns values with interpolation" do
58
+ assert_equal "#{Socket.gethostname}:27018", SecretConfig.fetch("mongo/secondary")
59
+ end
60
+
61
+ it "can be omitted an environment variable override with #check_env_var configuration" do
62
+ ENV["MYSQL_DATABASE"] = "other"
63
+
64
+ SecretConfig.check_env_var = false
65
+ SecretConfig.use :file, path: path, file_name: file_name
66
+ assert_equal "secret_config_test", SecretConfig.fetch("mysql/database")
67
+ end
50
68
  end
51
69
  end
52
70
  end
@@ -0,0 +1,152 @@
1
+ require_relative 'test_helper'
2
+ module SecretConfig
3
+ class SettingInterpolatorTest < Minitest::Test
4
+ describe SettingInterpolator do
5
+ let(:interpolator) { SettingInterpolator.new }
6
+
7
+ describe '#parse' do
8
+ it "handles good key" do
9
+ string = "Set a date of %{date} here."
10
+ expected = string.gsub("%{date}", Date.today.strftime("%Y%m%d"))
11
+ actual = interpolator.parse(string)
12
+ assert_equal expected, actual, string
13
+ end
14
+
15
+ it "handles multiple keys" do
16
+ string = "%{pid}: Set a date of %{date} here and a %{time:%H%M} here and for luck %{pid}"
17
+ expected = string.gsub("%{date}", Date.today.strftime("%Y%m%d"))
18
+ expected = expected.gsub("%{time:%H%M}", Time.now.strftime("%H%M"))
19
+ expected = expected.gsub("%{pid}", $$.to_s)
20
+ actual = interpolator.parse(string)
21
+ assert_equal expected, actual, string
22
+ end
23
+
24
+ it "handles bad key" do
25
+ string = "Set a date of %{blah} here."
26
+ assert_raises InvalidInterpolation do
27
+ interpolator.parse(string)
28
+ end
29
+ end
30
+ end
31
+
32
+ describe "#date" do
33
+ it 'interpolates date only' do
34
+ string = "%{date}"
35
+ expected = Date.today.strftime("%Y%m%d")
36
+ actual = interpolator.parse(string)
37
+ assert_equal expected, actual, string
38
+ end
39
+
40
+ it 'interpolates date' do
41
+ string = "Set a date of %{date} here."
42
+ expected = string.gsub("%{date}", Date.today.strftime("%Y%m%d"))
43
+ actual = interpolator.parse(string)
44
+ assert_equal expected, actual, string
45
+ end
46
+
47
+ it 'interpolates date with custom format' do
48
+ string = "Set a custom %{date:%m%d%Y} here."
49
+ expected = string.gsub("%{date:%m%d%Y}", Date.today.strftime("%m%d%Y"))
50
+ actual = interpolator.parse(string)
51
+ assert_equal expected, actual, string
52
+ end
53
+ end
54
+
55
+ describe "#time" do
56
+ it 'interpolates time only' do
57
+ string = "%{time}"
58
+ time = Time.now
59
+ Time.stub(:now, time) do
60
+ expected = Time.now.strftime("%Y%m%d%H%M%S%L")
61
+ actual = interpolator.parse(string)
62
+ assert_equal expected, actual, string
63
+ end
64
+ end
65
+
66
+ it 'interpolates time' do
67
+ string = "Set a time of %{time} here."
68
+ time = Time.now
69
+ Time.stub(:now, time) do
70
+ expected = string.gsub("%{time}", Time.now.strftime("%Y%m%d%H%M%S%L"))
71
+ actual = interpolator.parse(string)
72
+ assert_equal expected, actual, string
73
+ end
74
+ end
75
+
76
+ it 'interpolates time with custom format' do
77
+ string = "Set a custom time of %{time:%H%M} here."
78
+ expected = string.gsub("%{time:%H%M}", Time.now.strftime("%H%M"))
79
+ actual = interpolator.parse(string)
80
+ assert_equal expected, actual, string
81
+ end
82
+ end
83
+
84
+ describe "#env" do
85
+ before do
86
+ ENV["TEST_SETTING"] = "Secret"
87
+ end
88
+
89
+ it 'fetches existing ENV var' do
90
+ string = "%{env:TEST_SETTING}"
91
+ actual = interpolator.parse(string)
92
+ assert_equal "Secret", actual, string
93
+ end
94
+
95
+ it 'fetches existing ENV var into a larger string' do
96
+ string = "Hello %{env:TEST_SETTING}. How are you?"
97
+ actual = interpolator.parse(string)
98
+ expected = string.gsub("%{env:TEST_SETTING}", "Secret")
99
+ assert_equal expected, actual, string
100
+ end
101
+
102
+ it 'handles missing ENV var' do
103
+ string = "%{env:OTHER_TEST_SETTING}"
104
+ actual = interpolator.parse(string)
105
+ assert_equal "", actual, string
106
+ end
107
+ end
108
+
109
+ describe "#hostname" do
110
+ it 'returns hostname' do
111
+ string = "%{hostname}"
112
+ actual = interpolator.parse(string)
113
+ assert_equal Socket.gethostname, actual, string
114
+ end
115
+
116
+ it 'returns short hostname' do
117
+ string = "%{hostname:short}"
118
+ actual = interpolator.parse(string)
119
+ assert_equal Socket.gethostname.split('.')[0], actual, string
120
+ end
121
+ end
122
+
123
+ describe "#pid" do
124
+ it 'returns process id' do
125
+ string = "%{pid}"
126
+ actual = interpolator.parse(string)
127
+ assert_equal $$.to_s, actual, string
128
+ end
129
+ end
130
+
131
+ describe "#random" do
132
+ it 'interpolates random 32 byte string' do
133
+ string = "%{random}"
134
+ random = SecureRandom.urlsafe_base64(32)
135
+ SecureRandom.stub(:urlsafe_base64, random) do
136
+ actual = interpolator.parse(string)
137
+ assert_equal random, actual, string
138
+ end
139
+ end
140
+
141
+ it 'interpolates custom length random string' do
142
+ string = "%{random:64}"
143
+ random = SecureRandom.urlsafe_base64(64)
144
+ SecureRandom.stub(:urlsafe_base64, random) do
145
+ actual = interpolator.parse(string)
146
+ assert_equal random, actual, string
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: secret_config
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.4
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-21 00:00:00.000000000 Z
11
+ date: 2020-02-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -44,6 +44,8 @@ files:
44
44
  - lib/secret_config/providers/ssm.rb
45
45
  - lib/secret_config/railtie.rb
46
46
  - lib/secret_config/registry.rb
47
+ - lib/secret_config/setting_interpolator.rb
48
+ - lib/secret_config/string_interpolator.rb
47
49
  - lib/secret_config/utils.rb
48
50
  - lib/secret_config/version.rb
49
51
  - test/config/application.yml
@@ -51,6 +53,7 @@ files:
51
53
  - test/providers/ssm_test.rb
52
54
  - test/registry_test.rb
53
55
  - test/secret_config_test.rb
56
+ - test/setting_interpolator_test.rb
54
57
  - test/test_helper.rb
55
58
  - test/utils_test.rb
56
59
  homepage: https://github.com/rocketjob/secret_config
@@ -81,6 +84,7 @@ test_files:
81
84
  - test/providers/ssm_test.rb
82
85
  - test/providers/file_test.rb
83
86
  - test/registry_test.rb
87
+ - test/setting_interpolator_test.rb
84
88
  - test/test_helper.rb
85
89
  - test/utils_test.rb
86
90
  - test/secret_config_test.rb