Skip to content

Commit

Permalink
Add ability to save/restore ENV, automatically load in Rails
Browse files Browse the repository at this point in the history
  • Loading branch information
bkeepers committed Jan 21, 2024
1 parent f47d226 commit 6416aa5
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 41 deletions.
9 changes: 5 additions & 4 deletions Rakefile
@@ -1,6 +1,9 @@
#!/usr/bin/env rake

require "bundler/gem_helper"
require "rspec/core/rake_task"
require "rake/testtask"
require "standard/rake"

namespace "dotenv" do
Bundler::GemHelper.install_tasks name: "dotenv"
Expand All @@ -24,14 +27,12 @@ task build: ["dotenv:build", "dotenv-rails:build"]
task install: ["dotenv:install", "dotenv-rails:install"]
task release: ["dotenv:release", "dotenv-rails:release"]

require "rspec/core/rake_task"

desc "Run all specs"
RSpec::Core::RakeTask.new(:spec) do |t|
t.rspec_opts = %w[--color]
t.verbose = false
end

require "standard/rake"
Rake::TestTask.new

task default: [:spec, :standard]
task default: [:spec, :test, :standard]
13 changes: 12 additions & 1 deletion lib/dotenv.rb
Expand Up @@ -63,7 +63,7 @@ def instrument(name, payload = {}, &block)
if instrumenter
instrumenter.instrument(name, payload, &block)
else
yield
block&.call
end
end

Expand All @@ -72,6 +72,17 @@ def require_keys(*keys)
return if missing_keys.empty?
raise MissingKeys, missing_keys
end

# Save a snapshot of the current `ENV` to be restored later
def save
@snapshot = ENV.to_h.freeze
instrument("dotenv.save", env: @snapshot)
end

# Restore the previous snapshot of `ENV`
def restore
instrument("dotenv.restore", env: @snapshot) { ENV.replace(@snapshot) }
end
end

require "dotenv/rails" if defined?(Rails::Railtie)
25 changes: 16 additions & 9 deletions lib/dotenv/rails.rb
Expand Up @@ -5,7 +5,7 @@
# Watch all loaded env files with Spring
begin
require "spring/commands"
ActiveSupport::Notifications.subscribe(/^dotenv/) do |*args|
ActiveSupport::Notifications.subscribe('dotenv.load') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
Spring.watch event.payload[:env].filename if Rails.application
end
Expand All @@ -16,17 +16,20 @@
module Dotenv
# Rails integration for using Dotenv to load ENV variables from a file
class Rails < ::Rails::Railtie
attr_accessor :overwrite, :files
delegate :files, :files=, :overwrite, :overwrite=, :test_help, :test_help=, to: "config.dotenv"

def initialize
super()
@overwrite = false
@files = [
root.join(".env.#{env}.local"),
(root.join(".env.local") unless env.test?),
root.join(".env.#{env}"),
root.join(".env")
].compact
config.dotenv = ActiveSupport::OrderedOptions.new.update(
overwrite: false,
files: [
root.join(".env.#{env}.local"),
(root.join(".env.local") unless env.test?),
root.join(".env.#{env}"),
root.join(".env")
].compact,
test_help: env.test?
)
end

# Public: Load dotenv
Expand Down Expand Up @@ -85,6 +88,10 @@ def self.load
app.deprecators[:dotenv] = deprecator if app.respond_to?(:deprecators)
end

initializer "dotenv.test_help" do |app|
require "dotenv/test_help" if test_help
end

config.before_configuration { load }
end

Expand Down
21 changes: 21 additions & 0 deletions lib/dotenv/test_help.rb
@@ -0,0 +1,21 @@
if defined?(RSpec.configure)
RSpec.configure do |config|
# Save ENV before the suite starts
config.before(:suite) { Dotenv.save }

# Restore ENV after each example
config.after { Dotenv.restore }
end
end

if defined?(ActiveSupport)
ActiveSupport.on_load(:active_support_test_case) do
# Save ENV when the test suite loads
Dotenv.save

ActiveSupport::TestCase.class_eval do
# Restore ENV after each test
setup { Dotenv.restore }
end
end
end
71 changes: 54 additions & 17 deletions spec/dotenv/rails_spec.rb
Expand Up @@ -3,10 +3,31 @@
require "dotenv/rails"

describe Dotenv::Rails do
let(:application) do
Class.new(Rails::Application) do
config.load_defaults Rails::VERSION::STRING.to_f
config.eager_load = false
config.logger = ActiveSupport::Logger.new(StringIO.new)
config.root = fixture_path
end.instance
end

around do |example|
# These get frozen after the app initializes
autoload_paths = ActiveSupport::Dependencies.autoload_paths.dup
autoload_once_paths = ActiveSupport::Dependencies.autoload_once_paths.dup

# Run in fixtures directory
Dir.chdir(fixture_path) { example.run }
ensure
# Restore autoload paths to unfrozen state
ActiveSupport::Dependencies.autoload_paths = autoload_paths
ActiveSupport::Dependencies.autoload_once_paths = autoload_once_paths
end

before do
Rails.env = "test"
allow(Rails).to receive(:root).and_return Pathname.new(__dir__).join("../fixtures")
Rails.application = double(:application)
Rails.application = nil
Spring.watcher = Set.new # Responds to #add
end

Expand All @@ -15,22 +36,16 @@
Dotenv::Rails.remove_instance_variable(:@instance)
end

after do
# Reset
Spring.watcher = nil
Rails.application = nil
end

describe "files" do
it "loads files for development environment" do
Rails.env = "development"

expect(Dotenv::Rails.files).to eql(
[
Rails.root.join(".env.development.local"),
Rails.root.join(".env.local"),
Rails.root.join(".env.development"),
Rails.root.join(".env")
application.root.join(".env.development.local"),
application.root.join(".env.local"),
application.root.join(".env.development"),
application.root.join(".env")
]
)
end
Expand All @@ -39,15 +54,16 @@
Rails.env = "test"
expect(Dotenv::Rails.files).to eql(
[
Rails.root.join(".env.test.local"),
Rails.root.join(".env.test"),
Rails.root.join(".env")
application.root.join(".env.test.local"),
application.root.join(".env.test"),
application.root.join(".env")
]
)
end
end

it "watches loaded files with Spring" do
it "watches other loaded files with Spring" do
application.initialize!
path = fixture_path("plain.env")
Dotenv.load(path)
expect(Spring.watcher).to include(path.to_s)
Expand All @@ -61,7 +77,7 @@
end

context "load" do
subject { Dotenv::Rails.load }
subject { application.initialize! }

it "watches .env with Spring" do
subject
Expand Down Expand Up @@ -109,4 +125,25 @@
end
end
end

describe "test_help" do
it "is loaded if RAILS_ENV=test" do
expect(Dotenv::Rails.test_help).to eq(true)
expect(Dotenv::Rails.instance).to receive(:require).with("dotenv/test_help")
application.initialize!
end

it "is not loaded if RAILS_ENV=development" do
Rails.env = "development"
expect(Dotenv::Rails.test_help).to eq(false)
expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/test_help")
application.initialize!
end

it "is not loaded if test_help set to false" do
Dotenv::Rails.test_help = false
expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/test_help")
application.initialize!
end
end
end
10 changes: 8 additions & 2 deletions spec/dotenv_spec.rb
Expand Up @@ -94,7 +94,10 @@

it "fails silently" do
expect { subject }.not_to raise_error
expect(ENV.keys).to eq(@env_keys)
end

it "does not change ENV" do
expect { subject }.not_to change { ENV.inspect }
end
end
end
Expand Down Expand Up @@ -149,7 +152,10 @@

it "fails silently" do
expect { subject }.not_to raise_error
expect(ENV.keys).to eq(@env_keys)
end

it "does not change ENV" do
expect { subject }.not_to change { ENV.inspect }
end
end
end
Expand Down
11 changes: 3 additions & 8 deletions spec/spec_helper.rb
@@ -1,11 +1,6 @@
require "dotenv"
require "dotenv/test_help"

RSpec.configure do |config|
# Restore the state of ENV after each spec
config.before { @env_keys = ENV.keys }
config.after { ENV.delete_if { |k, _v| !@env_keys.include?(k) } }
end

def fixture_path(name)
Pathname.new(__dir__).join("./fixtures", name)
def fixture_path(*parts)
Pathname.new(__dir__).join("./fixtures", *parts)
end
18 changes: 18 additions & 0 deletions test/test_help_test.rb
@@ -0,0 +1,18 @@
require "active_support/deprecator"
require "active_support/test_case"
require "minitest/autorun"

require "dotenv"
require "dotenv/test_help"

class TestHelpTest < ActiveSupport::TestCase
test "restores ENV between tests, part 1" do
assert_nil ENV["DOTENV"], "ENV was not restored between tests"
ENV["DOTENV"] = "1"
end

test "restores ENV between tests, part 2" do
assert_nil ENV["DOTENV"], "ENV was not restored between tests"
ENV["DOTENV"] = "2"
end
end

0 comments on commit 6416aa5

Please sign in to comment.