diff --git a/CHANGELOG.md b/CHANGELOG.md index fcaa7c5..29c85d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,17 @@ Release notes are available on [github][notes]. - [fix] Replace `assert()` with explicit `if`/`throw` in `getInt()`, `getDouble()`, and `getBool()` so null-safety checks are enforced in release builds - Error messages now include the variable name for easier debugging +- [fix] `load(isOptional: true)` no longer discards successfully loaded base file when an override file is missing or empty (fixes #70, #93, #101, #125) +- [fix] `clean()` now resets `isInitialized` to `false`, so accessing `env` after `clean()` correctly throws `NotInitializedError` ### Note on release-build behavior change In **debug mode**, behavior is unchanged — `AssertionError` was thrown before, `AssertionError` is thrown now. In **release mode**, calling `getInt()`, `getDouble()`, or `getBool()` with a missing variable and no fallback previously threw a `TypeError` (from the null-check operator `!`) because `assert()` was stripped. It now correctly throws `AssertionError` with a descriptive message. If your release-mode code catches `on TypeError` around these methods, update it to catch `on AssertionError` (or `on Error`) instead. +### Breaking change: `clean()` now resets initialization state +Previously, calling `clean()` only cleared the env map but left `isInitialized == true`. Now it also sets `isInitialized = false`. Code that calls `clean()` and then immediately accesses `dotenv.env` without reloading will now throw `NotInitializedError`. The fix is to call `load()` or `loadFromString()` again after `clean()`. + # 6.0.0 - [feat] Allow passing in override .env files on init diff --git a/lib/src/dotenv.dart b/lib/src/dotenv.dart index e246f17..c97635e 100644 --- a/lib/src/dotenv.dart +++ b/lib/src/dotenv.dart @@ -45,8 +45,11 @@ class DotEnv { bool get isInitialized => _isInitialized; - /// Clear [env] - void clean() => _envMap.clear(); + /// Clear [env] and reset initialization state + void clean() { + _envMap.clear(); + _isInitialized = false; + } String get(String name, {String? fallback}) { final value = maybeGet(name, fallback: fallback); @@ -134,19 +137,26 @@ class DotEnv { Parser parser = const Parser(), }) async { clean(); - List linesFromFile; - List linesFromOverrides; + List linesFromFile = []; + List linesFromOverrides = []; + try { linesFromFile = await _getEntriesFromFile(fileName); - linesFromOverrides = await _getLinesFromOverride(overrideWithFiles); } on FileNotFoundError { if (!isOptional) rethrow; - linesFromFile = []; - linesFromOverrides = []; } on EmptyEnvFileError { if (!isOptional) rethrow; - linesFromFile = []; - linesFromOverrides = []; + } + + for (final overrideFile in overrideWithFiles) { + try { + final lines = await _getEntriesFromFile(overrideFile); + linesFromOverrides.addAll(lines); + } on FileNotFoundError { + if (!isOptional) rethrow; + } on EmptyEnvFileError { + if (!isOptional) rethrow; + } } final linesFromMergeWith = mergeWith.entries @@ -214,15 +224,4 @@ class DotEnv { } } - Future> _getLinesFromOverride(List overrideWith) async { - List overrideLines = []; - - for (int i = 0; i < overrideWith.length; i++) { - final overrideWithFile = overrideWith[i]; - final lines = await _getEntriesFromFile(overrideWithFile); - overrideLines = overrideLines..addAll(lines); - } - - return overrideLines; - } } diff --git a/test/dotenv_test.dart b/test/dotenv_test.dart index 0e289f9..67c3174 100644 --- a/test/dotenv_test.dart +++ b/test/dotenv_test.dart @@ -150,4 +150,66 @@ void main() { e.message.toString().contains('MISSING_VAR')))); }); }); + + group('clean() resets initialization state', () { + test('after clean(), isInitialized is false', () { + dotenv.loadFromString(envString: 'FOO=bar'); + expect(dotenv.isInitialized, isTrue); + dotenv.clean(); + expect(dotenv.isInitialized, isFalse); + }); + + test('after clean(), accessing env throws NotInitializedError', () { + dotenv.loadFromString(envString: 'FOO=bar'); + expect(dotenv.env['FOO'], 'bar'); + dotenv.clean(); + expect(() => dotenv.env, throwsA(isA())); + }); + + test( + 'loadFromString after clean() re-initializes correctly', () { + dotenv.loadFromString(envString: 'FOO=bar'); + dotenv.clean(); + dotenv.loadFromString(envString: 'BAZ=qux'); + expect(dotenv.isInitialized, isTrue); + expect(dotenv.env['BAZ'], 'qux'); + expect(dotenv.env['FOO'], isNull); + }); + }); + + group('isOptional override loading', () { + test( + 'loadFromString with valid base and empty override preserves base vars', + () { + dotenv.loadFromString( + envString: 'FOO=bar\nBAZ=qux', + overrideWith: [''], + isOptional: true, + ); + expect(dotenv.env['FOO'], 'bar'); + expect(dotenv.env['BAZ'], 'qux'); + }); + + test('loadFromString with empty base and valid override preserves override', + () { + dotenv.loadFromString( + envString: '', + overrideWith: ['OVERRIDE_KEY=override_value'], + isOptional: true, + ); + expect(dotenv.env['OVERRIDE_KEY'], 'override_value'); + }); + + test( + 'loadFromString with isOptional=false and empty base throws even with valid override', + () { + expect( + () => dotenv.loadFromString( + envString: '', + overrideWith: ['OVERRIDE_KEY=override_value'], + isOptional: false, + ), + throwsA(isA())); + }); + }); } diff --git a/test/empty_env_test.dart b/test/empty_env_test.dart index 01d9e14..68b56ce 100644 --- a/test/empty_env_test.dart +++ b/test/empty_env_test.dart @@ -30,7 +30,7 @@ void main() { }, throwsA(isA())); }); - test('missing .env file with isOptional=true should not throw', () { + test('empty string with isOptional=true should not throw', () { File emptyFile = File('test/.env.empty'); expect(() { dotenv.loadFromString( @@ -41,8 +41,7 @@ void main() { expect(dotenv.env.isEmpty, isTrue); }); - test( - 'missing .env file with isOptional=false should throw FileNotFoundError', + test('empty string with isOptional=false should throw EmptyEnvFileError', () { expect(() { dotenv.loadFromString(envString: '', isOptional: false);