diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3652804..a698370 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: push: branches: [ master ] pull_request: - workflow_dispatch: # Allows manual triggering + workflow_dispatch: jobs: build-and-test: @@ -12,22 +12,22 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Use .NET 8 SDK + - name: Use .NET 9 SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.x - name: Restore dependencies - run: dotnet restore ./src/**/*.csproj + run: dotnet restore - name: Build all projects - run: dotnet build ./src/**/*.csproj -c Release --no-restore + run: dotnet build --no-restore - name: Run tests with code coverage - run: dotnet test dotenv.net.sln /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=./coverage/ + run: dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=./coverage/ - name: Upload coverage to Coveralls uses: coverallsapp/github-action@v1.1.2 with: github-token: ${{ github.token }} - path-to-lcov: ${{ github.workspace }}/tests/dotenv.net.Tests/coverage/coverage.info + path-to-lcov: ${{ github.workspace }}/src/dotenv.net.Tests/coverage/coverage.info diff --git a/dotenv.net.sln b/dotenv.net.sln index f38f28e..33d9e2b 100644 --- a/dotenv.net.sln +++ b/dotenv.net.sln @@ -1,13 +1,10 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2013 VisualStudioVersion = 12.0.0.0 MinimumVisualStudioVersion = 10.0.0.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotenv.net", "src\dotenv.net/dotenv.net.csproj", "{92FD0370-AEB1-4154-8C9F-D5FF30CABCF8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotenv.net", "src\dotenv.net\dotenv.net.csproj", "{ABC30C21-62C1-4DF5-B352-4D33BCDD9845}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotenv.net.Tests", "tests\dotenv.net.Tests\dotenv.net.Tests.csproj", "{A03D81EE-4F67-4521-932A-0B9B5BE50F72}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "tests", "{1CEC83AC-9E34-4320-AECE-F0F52FE70538}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotenv.net.Tests", "src\dotenv.net.Tests\dotenv.net.Tests.csproj", "{FC5ED4B3-DFDC-45E9-889C-847433F08E0E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,19 +12,13 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {92FD0370-AEB1-4154-8C9F-D5FF30CABCF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {92FD0370-AEB1-4154-8C9F-D5FF30CABCF8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {92FD0370-AEB1-4154-8C9F-D5FF30CABCF8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {92FD0370-AEB1-4154-8C9F-D5FF30CABCF8}.Release|Any CPU.Build.0 = Release|Any CPU - {A03D81EE-4F67-4521-932A-0B9B5BE50F72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A03D81EE-4F67-4521-932A-0B9B5BE50F72}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A03D81EE-4F67-4521-932A-0B9B5BE50F72}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A03D81EE-4F67-4521-932A-0B9B5BE50F72}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {A03D81EE-4F67-4521-932A-0B9B5BE50F72} = {1CEC83AC-9E34-4320-AECE-F0F52FE70538} + {ABC30C21-62C1-4DF5-B352-4D33BCDD9845}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ABC30C21-62C1-4DF5-B352-4D33BCDD9845}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ABC30C21-62C1-4DF5-B352-4D33BCDD9845}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ABC30C21-62C1-4DF5-B352-4D33BCDD9845}.Release|Any CPU.Build.0 = Release|Any CPU + {FC5ED4B3-DFDC-45E9-889C-847433F08E0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC5ED4B3-DFDC-45E9-889C-847433F08E0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC5ED4B3-DFDC-45E9-889C-847433F08E0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC5ED4B3-DFDC-45E9-889C-847433F08E0E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/dotenv.net.Tests/.env b/src/dotenv.net.Tests/.env new file mode 100644 index 0000000..29633a5 --- /dev/null +++ b/src/dotenv.net.Tests/.env @@ -0,0 +1,15 @@ +lower_case_key=world +# this is a comment +DOUBLE_QUOTES="double" +SINGLE_QUOTES='single' +BOOLEAN=true +NUMERIC=34.56 + DB_DATABASE= laravel +DOTTED.KEY =spaced value +=ValueWithNoKey +KeyWithNoValue= +DOUBLE_QUOTE_EVEN_MORE_LINES="this +is +\"a +multi-line + value" \ No newline at end of file diff --git a/src/dotenv.net.Tests/DotEnvOptionsTests.cs b/src/dotenv.net.Tests/DotEnvOptionsTests.cs new file mode 100644 index 0000000..5dc29e9 --- /dev/null +++ b/src/dotenv.net.Tests/DotEnvOptionsTests.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Shouldly; +using Xunit; + +namespace dotenv.net.Tests; + +public class DotEnvOptionsTests +{ + [Fact] + public void Constructor_WithNullEnvFilePaths_ShouldUseDefaultPath() + { + var options = new DotEnvOptions(envFilePaths: null); + options.EnvFilePaths.ShouldBe([DotEnvOptions.DefaultEnvFileName]); + } + + [Fact] + public void Constructor_WithEmptyEnvFilePaths_ShouldUseDefaultPath() + { + var emptyPaths = new List(); + var options = new DotEnvOptions(envFilePaths: emptyPaths); + options.EnvFilePaths.ShouldBe([DotEnvOptions.DefaultEnvFileName]); + } + + [Fact] + public void Constructor_WithNullEncoding_ShouldUseUtf8() + { + var options = new DotEnvOptions(encoding: null); + options.Encoding.ShouldBe(Encoding.UTF8); + } + + [Fact] + public void WithEncoding_WithNullEncoding_ShouldUseUtf8() + { + var options = new DotEnvOptions(); + options.WithEncoding(null!); + options.Encoding.ShouldBe(Encoding.UTF8); + } + + [Fact] + public void WithEnvFiles_WithNullParams_ShouldUseDefaultPath() + { + var options = new DotEnvOptions(); + options.WithEnvFiles(null!); + options.EnvFilePaths.ShouldBe([DotEnvOptions.DefaultEnvFileName]); + } + + [Fact] + public void WithEnvFiles_WithEmptyParams_ShouldUseDefaultPath() + { + var options = new DotEnvOptions(); + options.WithEnvFiles(Array.Empty()); + options.EnvFilePaths.ShouldBe([DotEnvOptions.DefaultEnvFileName]); + } + + [Fact] + public void WithProbeForEnv_WithNegativeProbeLevels_ShouldUseDefaultProbeDepth() + { + var options = new DotEnvOptions(); + options.WithProbeForEnv(-1); + options.ProbeLevelsToSearch.ShouldBe(DotEnvOptions.DefaultProbeAscendLimit); + } + + [Fact] + public void WithoutProbeForEnv_ShouldResetProbeLevelsToDefault() + { + var options = new DotEnvOptions().WithProbeForEnv(10); + options.WithoutProbeForEnv(); + options.ProbeForEnv.ShouldBeFalse(); + options.ProbeLevelsToSearch.ShouldBe(DotEnvOptions.DefaultProbeAscendLimit); + } + + [Fact] + public void WithDefaultEncoding_ShouldResetToUtf8() + { + var options = new DotEnvOptions().WithEncoding(Encoding.ASCII); + options.WithDefaultEncoding(); + options.Encoding.ShouldBe(Encoding.UTF8); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void WithExceptions_ShouldSetIgnoreExceptionsCorrectly(bool initialValue) + { + var options = new DotEnvOptions(ignoreExceptions: initialValue); + options.WithExceptions(); + options.IgnoreExceptions.ShouldBeFalse(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void WithoutExceptions_ShouldSetIgnoreExceptionsCorrectly(bool initialValue) + { + var options = new DotEnvOptions(ignoreExceptions: initialValue); + options.WithoutExceptions(); + options.IgnoreExceptions.ShouldBeTrue(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void WithOverwriteExistingVars_ShouldSetOverwriteCorrectly(bool initialValue) + { + var options = new DotEnvOptions(overwriteExistingVars: initialValue); + options.WithOverwriteExistingVars(); + options.OverwriteExistingVars.ShouldBeTrue(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void WithoutOverwriteExistingVars_ShouldSetOverwriteCorrectly(bool initialValue) + { + var options = new DotEnvOptions(overwriteExistingVars: initialValue); + options.WithoutOverwriteExistingVars(); + options.OverwriteExistingVars.ShouldBeFalse(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void WithTrimValues_ShouldSetTrimValuesCorrectly(bool initialValue) + { + var options = new DotEnvOptions(trimValues: initialValue); + options.WithTrimValues(); + options.TrimValues.ShouldBeTrue(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void WithoutTrimValues_ShouldSetTrimValuesCorrectly(bool initialValue) + { + var options = new DotEnvOptions(trimValues: initialValue); + options.WithoutTrimValues(); + options.TrimValues.ShouldBeFalse(); + } + + [Fact] + public void Read_ComplexExistingEnv_ShouldExtractValidValues() + { + var values = DotEnv.Fluent() + .WithTrimValues() + .WithProbeForEnv() + .Read(); + + values.ShouldContainKeyAndValue("lower_case_key", "world"); + values.ShouldContainKeyAndValue("DOUBLE_QUOTES", "double"); + values.ShouldContainKeyAndValue("SINGLE_QUOTES", "single"); + values.ShouldContainKeyAndValue("BOOLEAN", "true"); + values.ShouldContainKeyAndValue("NUMERIC", "34.56"); + values.ShouldContainKeyAndValue("DOTTED.KEY", "spaced value"); + values.ShouldContainKeyAndValue("KeyWithNoValue", string.Empty); + values.ShouldContainKeyAndValue("DOUBLE_QUOTE_EVEN_MORE_LINES", + $"""this{Environment.NewLine}is{Environment.NewLine}"a{Environment.NewLine}multi-line{Environment.NewLine} value"""); + } +} diff --git a/src/dotenv.net.Tests/DotEnvTests.cs b/src/dotenv.net.Tests/DotEnvTests.cs new file mode 100644 index 0000000..21d882a --- /dev/null +++ b/src/dotenv.net.Tests/DotEnvTests.cs @@ -0,0 +1,25 @@ +using System; +using Shouldly; +using Xunit; + +namespace dotenv.net.Tests; + +public class DotEnvTests +{ + [Fact] + public void Read_ComplexExistingEnv_ShouldExtractValidValues() + { + var options = new DotEnvOptions(trimValues: true, probeForEnv: true, probeLevelsToSearch: 5); + var values = DotEnv.Read(options); + + values.ShouldContainKeyAndValue("lower_case_key", "world"); + values.ShouldContainKeyAndValue("DOUBLE_QUOTES", "double"); + values.ShouldContainKeyAndValue("SINGLE_QUOTES", "single"); + values.ShouldContainKeyAndValue("BOOLEAN", "true"); + values.ShouldContainKeyAndValue("NUMERIC", "34.56"); + values.ShouldContainKeyAndValue("DOTTED.KEY", "spaced value"); + values.ShouldContainKeyAndValue("KeyWithNoValue", string.Empty); + values.ShouldContainKeyAndValue("DOUBLE_QUOTE_EVEN_MORE_LINES", + $"""this{Environment.NewLine}is{Environment.NewLine}"a{Environment.NewLine}multi-line{Environment.NewLine} value"""); + } +} \ No newline at end of file diff --git a/src/dotenv.net.Tests/ParserTests.cs b/src/dotenv.net.Tests/ParserTests.cs new file mode 100644 index 0000000..ebad0a4 --- /dev/null +++ b/src/dotenv.net.Tests/ParserTests.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using Shouldly; +using Xunit; + +namespace dotenv.net.Tests; + +public class ParserTests +{ + [Fact] + public void Parse_EmptyLines_ShouldBeIgnored() + { + var lines = new[] { "", " ", null, "KEY=value" }; + var result = Parser.Parse(lines, trimValues: false).ToArray(); + result.Length.ShouldBe(1); + result[0].ShouldBe(new KeyValuePair("KEY", "value")); + } + + [Fact] + public void Parse_CommentLines_ShouldBeIgnored() + { + var lines = new[] { "# Comment", " # Indented comment", "KEY=value" }; + var result = Parser.Parse(lines, trimValues: false).ToArray(); + result.Length.ShouldBe(1); + result[0].ShouldBe(new KeyValuePair("KEY", "value")); + } + + [Fact] + public void Parse_LinesWithoutKey_ShouldBeIgnored() + { + var lines = new[] { "=value", "NOKEY", "KEY=value" }; + var result = Parser.Parse(lines, trimValues: false).ToArray(); + result.Length.ShouldBe(1); + result[0].ShouldBe(new KeyValuePair("KEY", "value")); + } + + [Fact] + public void Parse_SimpleKeyValue_ShouldReturnPair() + { + var lines = new[] { "TEST_KEY=test_value" }; + var result = Parser.Parse(lines, trimValues: false).ToArray(); + result.Length.ShouldBe(1); + result[0].ShouldBe(new KeyValuePair("TEST_KEY", "test_value")); + } + + [Fact] + public void Parse_UntrimmedValueWithTrimValuesFalse_ShouldPreserveWhitespace() + { + var lines = new[] { " KEY = value " }; + var result = Parser.Parse(lines, trimValues: false).ToArray(); + result.Length.ShouldBe(1); + result[0].ShouldBe(new KeyValuePair("KEY", " value ")); + } + + [Fact] + public void Parse_UntrimmedValueWithTrimValuesTrue_ShouldTrimValue() + { + var lines = new[] { "KEY= value " }; + var result = Parser.Parse(lines, trimValues: true).ToArray(); + result.Length.ShouldBe(1); + result[0].ShouldBe(new KeyValuePair("KEY", "value")); + } + + [Fact] + public void Parse_SingleQuotedValue_ShouldUnescapeQuotes() + { + var lines = new[] { "KEY='value with \\' quote'" }; + var result = Parser.Parse(lines, trimValues: false).ToArray(); + result.Length.ShouldBe(1); + result[0].ShouldBe(new KeyValuePair("KEY", "value with ' quote")); + } + + [Fact] + public void Parse_DoubleQuotedValue_ShouldUnescapeQuotes() + { + var lines = new[] { "KEY=\"value with \\\" quote\"" }; + var result = Parser.Parse(lines, trimValues: false).ToArray(); + result.Length.ShouldBe(1); + result[0].ShouldBe(new KeyValuePair("KEY", "value with \" quote")); + } + + [Fact] + public void Parse_EscapedBackslashes_ShouldUnescape() + { + var lines = new[] { "KEY='escaped \\\\ backslash'" }; + var result = Parser.Parse(lines, trimValues: false).ToArray(); + result.Length.ShouldBe(1); + result.ShouldContain(new KeyValuePair("KEY", "escaped \\ backslash")); + } + + [Fact] + public void Parse_MultiLineValue_ShouldCombineLines() + { + var lines = new[] { "KEY='first line", "second line'", "NEXT=value" }; + var result = Parser.Parse(lines, trimValues: false).ToArray(); + result.Length.ShouldBe(2); + result[0].ShouldBe(new KeyValuePair("KEY", $"first line{Environment.NewLine}second line")); + result[1].ShouldBe(new KeyValuePair("NEXT", "value")); + } + + [Fact] + public void Parse_UnclosedQuote_ShouldThrowException() + { + var lines = new[] { "KEY='unclosed quote" }; + Action act = () => Parser.Parse(lines, trimValues: false).ToArray(); + act.ShouldThrow() + .Message.ShouldBe("Unable to parse environment variable: KEY. Missing closing quote."); + } + + [Fact] + public void Parse_EscapedQuoteInMiddle_ShouldUnescape() + { + var lines = new[] { "KEY='before\\'after'" }; + var result = Parser.Parse(lines, trimValues: false).ToArray(); + result.Length.ShouldBe(1); + result.ShouldContain(new KeyValuePair("KEY", "before'after")); + } + + [Fact] + public void Parse_BackslashNotEscapingQuote_ShouldRemain() + { + var lines = new[] { "KEY='before\\after'" }; + var result = Parser.Parse(lines, trimValues: false).ToArray(); + result.Length.ShouldBe(1); + result.ShouldContain(new KeyValuePair("KEY", "before\\after")); + } +} diff --git a/src/dotenv.net.Tests/ReaderTests.cs b/src/dotenv.net.Tests/ReaderTests.cs new file mode 100644 index 0000000..b0cbe5a --- /dev/null +++ b/src/dotenv.net.Tests/ReaderTests.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Shouldly; +using Xunit; + +namespace dotenv.net.Tests; + +public class ReaderTests : IDisposable +{ + private readonly string _tempFilePath; + private readonly string _tempDirPath; + + public ReaderTests() + { + _tempFilePath = Path.GetTempFileName(); + _tempDirPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(_tempDirPath); + } + + public void Dispose() + { + if (File.Exists(_tempFilePath)) + File.Delete(_tempFilePath); + + if (Directory.Exists(_tempDirPath)) + Directory.Delete(_tempDirPath, true); + } + + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData(" ", false)] + public void ReadFileLines_InvalidPathAndIgnoreExceptionsFalse_ShouldThrowArgumentException(string path, bool ignoreExceptions) + { + Action act = () => Reader.ReadFileLines(path, ignoreExceptions, null); + act.ShouldThrow().Message.ShouldContain("The file path cannot be null, empty or whitespace."); + } + + [Theory] + [InlineData(null, true)] + [InlineData("", true)] + [InlineData(" ", true)] + public void ReadFileLines_InvalidPathAndIgnoreExceptionsTrue_ShouldReturnEmptySpan(string path, bool ignoreExceptions) + { + var result = Reader.ReadFileLines(path, ignoreExceptions, null).ToArray(); + result.ShouldBeEmpty(); + } + + [Fact] + public void ReadFileLines_NonExistentFileAndIgnoreExceptionsFalse_ShouldThrowFileNotFoundException() + { + var path = "nonexistent.env"; + Action act = () => Reader.ReadFileLines(path, false, null); + act.ShouldThrow().Message.ShouldContain (path); + } + + [Fact] + public void ReadFileLines_NonExistentFileAndIgnoreExceptionsTrue_ShouldReturnEmptySpan() + { + var result = Reader.ReadFileLines("nonexistent.env", true, null).ToArray(); + result.ShouldBeEmpty(); + } + + [Fact] + public void ReadFileLines_ValidFile_ShouldReturnLines() + { + File.WriteAllLines(_tempFilePath, ["KEY1=value1", "KEY2=value2"]); + var result = Reader.ReadFileLines(_tempFilePath, false, null); + result.Length.ShouldBe(2); + result[0].ShouldBe("KEY1=value1"); + result[1].ShouldBe("KEY2=value2"); + } + + [Fact] + public void ReadFileLines_WithCustomEncoding_ShouldReturnCorrectContent() + { + var content = "KEY=üñîçø∂é"; + File.WriteAllText(_tempFilePath, content, Encoding.UTF32); + var result = Reader.ReadFileLines(_tempFilePath, false, Encoding.UTF32); + result[0].ShouldBe(content); + } + + [Fact] + public void ExtractEnvKeyValues_EmptySpan_ShouldReturnEmptySpan() + { + var result = Reader.ExtractEnvKeyValues(ReadOnlySpan.Empty, false).ToArray(); + result.ShouldBeEmpty(); + } + + [Fact] + public void ExtractEnvKeyValues_ValidLines_ShouldReturnKeyValuePairs() + { + var lines = new[] { "KEY1=value1", "KEY2=value2" }; + var result = Reader.ExtractEnvKeyValues(lines, false); + result.Length.ShouldBe(2); + result[0].ShouldBe(new KeyValuePair("KEY1", "value1")); + result[1].ShouldBe(new KeyValuePair("KEY2", "value2")); + } + + [Fact] + public void MergeEnvKeyValues_NoArrays_ShouldReturnEmptyDictionary() + { + var result = Reader.MergeEnvKeyValues(new List[]>(), false); + result.ShouldBeEmpty(); + } + + [Fact] + public void MergeEnvKeyValues_SingleArray_ShouldReturnAllItems() + { + var input = new[] { + new[] { new KeyValuePair("KEY1", "value1"), new KeyValuePair("KEY2", "value2") } + }; + var result = Reader.MergeEnvKeyValues(input, false); + result.ShouldBe(new Dictionary { { "KEY1", "value1" }, { "KEY2", "value2" } }); + } + + [Fact] + public void MergeEnvKeyValues_MultipleArraysWithoutOverwrite_ShouldKeepFirstValue() + { + var input = new[] { + new[] { new KeyValuePair("KEY", "first") }, + new[] { new KeyValuePair("KEY", "second") } + }; + var result = Reader.MergeEnvKeyValues(input, false); + result.ShouldContainKey("KEY"); + result["KEY"].ShouldBe("first"); + } + + [Fact] + public void MergeEnvKeyValues_MultipleArraysWithOverwrite_ShouldKeepLastValue() + { + var input = new[] { + new[] { new KeyValuePair("KEY", "first") }, + new[] { new KeyValuePair("KEY", "second") } + }; + var result = Reader.MergeEnvKeyValues(input, true); + result.ShouldContainKey("KEY"); + result["KEY"].ShouldBe("second"); + } + + [Fact] + public void MergeEnvKeyValues_ComplexMerge_ShouldHandleAllCases() + { + var input = new[] { + new[] { + new KeyValuePair("KEY1", "value1"), + new KeyValuePair("KEY2", "value2") + }, + new[] { + new KeyValuePair("KEY2", "updated"), + new KeyValuePair("KEY3", "value3") + } + }; + var result = Reader.MergeEnvKeyValues(input, true); + result.ShouldBe(new Dictionary { { "KEY1", "value1" }, { "KEY2", "updated" }, { "KEY3", "value3" } }); + } + + [Fact] + public void GetProbedEnvPath_FileNotFoundAndIgnoreExceptionsFalse_ShouldThrow() + { + using var dir = new TempWorkingDirectory(_tempDirPath); + Action act = () => Reader.GetProbedEnvPath(levelsToSearch: 2, ignoreExceptions: false); + act.ShouldThrow() + .Message.ShouldContain(DotEnvOptions.DefaultEnvFileName); + } + + [Fact] + public void GetProbedEnvPath_FileNotFoundAndIgnoreExceptionsTrue_ShouldReturnNull() + { + using var dir = new TempWorkingDirectory(_tempDirPath); + var result = Reader.GetProbedEnvPath(levelsToSearch: 2, ignoreExceptions: true); + result.ShouldBeNull(); + } + + [Fact] + public void GetProbedEnvPath_LevelsTooLow_ShouldNotFindFile() + { + var envPath = Path.Combine(_tempDirPath, ".env"); + File.WriteAllText(envPath, "TEST=value"); + var startDir = Path.Combine(_tempDirPath, "subdir1", "subdir2", "subdir3"); + Directory.CreateDirectory(startDir); + + using var dir = new TempWorkingDirectory(startDir); + var result = Reader.GetProbedEnvPath(levelsToSearch: 2, ignoreExceptions: true); + result.ShouldBeNull(); + } + + private class TempWorkingDirectory : IDisposable + { + private readonly string _originalDirectory; + + public TempWorkingDirectory(string path) + { + _originalDirectory = Directory.GetCurrentDirectory(); + Directory.SetCurrentDirectory(path); + } + + public void Dispose() => Directory.SetCurrentDirectory(_originalDirectory); + } +} \ No newline at end of file diff --git a/src/dotenv.net.Tests/Utilities/EnvReaderTests.cs b/src/dotenv.net.Tests/Utilities/EnvReaderTests.cs new file mode 100644 index 0000000..498e441 --- /dev/null +++ b/src/dotenv.net.Tests/Utilities/EnvReaderTests.cs @@ -0,0 +1,154 @@ +using System; +using dotenv.net.Utilities; +using Shouldly; +using Xunit; + +namespace dotenv.net.Tests.Utilities; + +public class EnvReaderTests +{ + private const string TestKey = "TEST_ENV_KEY"; + + [Fact] + public void GetStringValue_KeyExists_ReturnsValue() + { + SetTestVariable("ValidValue"); + var result = EnvReader.GetStringValue(TestKey); + result.ShouldBe("ValidValue"); + } + + [Fact] + public void GetStringValue_KeyMissing_ThrowsException() + { + ClearTestVariable(); + Assert.Throws(() => EnvReader.GetStringValue(TestKey)); + } + + [Fact] + public void GetIntValue_ValidInteger_ReturnsValue() + { + SetTestVariable("42"); + var result = EnvReader.GetIntValue(TestKey); + result.ShouldBe(42); + } + + [Fact] + public void GetIntValue_InvalidInteger_ThrowsException() + { + SetTestVariable("Invalid"); + Assert.Throws(() => EnvReader.GetIntValue(TestKey)); + } + + [Fact] + public void GetDoubleValue_ValidDouble_ReturnsValue() + { + SetTestVariable("3.14"); + var result = EnvReader.GetDoubleValue(TestKey); + result.ShouldBe(3.14); + } + + [Fact] + public void GetDoubleValue_InvalidDouble_ThrowsException() + { + SetTestVariable("Invalid"); + Assert.Throws(() => EnvReader.GetDoubleValue(TestKey)); + } + + [Fact] + public void GetDecimalValue_ValidDecimal_ReturnsValue() + { + SetTestVariable("99.99"); + var result = EnvReader.GetDecimalValue(TestKey); + result.ShouldBe(99.99m); + } + + [Fact] + public void GetDecimalValue_InvalidDecimal_ThrowsException() + { + SetTestVariable("Invalid"); + Assert.Throws(() => EnvReader.GetDecimalValue(TestKey)); + } + + [Theory] + [InlineData("true", true)] + [InlineData("false", false)] + [InlineData("True", true)] + [InlineData("False", false)] + public void GetBooleanValue_ValidBool_ReturnsValue(string input, bool expected) + { + SetTestVariable(input); + var result = EnvReader.GetBooleanValue(TestKey); + result.ShouldBe(expected); + } + + [Fact] + public void GetBooleanValue_InvalidBool_ThrowsException() + { + SetTestVariable("Invalid"); + Assert.Throws(() => EnvReader.GetBooleanValue(TestKey)); + } + + [Fact] + public void TryGetStringValue_KeyExists_ReturnsTrueAndValue() + { + SetTestVariable("ValidValue"); + var success = EnvReader.TryGetStringValue(TestKey, out var value); + success.ShouldBeTrue(); + value.ShouldBe("ValidValue"); + } + + [Fact] + public void TryGetStringValue_KeyMissing_ReturnsFalseAndNull() + { + ClearTestVariable(); + var success = EnvReader.TryGetStringValue(TestKey, out var value); + success.ShouldBeFalse(); + value.ShouldBeNull(); + } + + [Fact] + public void TryGetIntValue_ValidInteger_ReturnsTrueAndValue() + { + SetTestVariable("42"); + var success = EnvReader.TryGetIntValue(TestKey, out var value); + success.ShouldBeTrue(); + value.ShouldBe(42); + } + + [Fact] + public void TryGetIntValue_InvalidInteger_ReturnsFalseAndDefault() + { + SetTestVariable("Invalid"); + var success = EnvReader.TryGetIntValue(TestKey, out var value); + success.ShouldBeFalse(); + value.ShouldBe(0); + } + + [Fact] + public void HasValue_KeyExists_ReturnsTrue() + { + SetTestVariable("AnyValue"); + var result = EnvReader.HasValue(TestKey); + result.ShouldBeTrue(); + } + + [Fact] + public void HasValue_KeyMissing_ReturnsFalse() + { + ClearTestVariable(); + var result = EnvReader.HasValue(TestKey); + result.ShouldBeFalse(); + } + + [Fact] + public void HasValue_KeyExistsEmptyValue_ReturnsFalse() + { + SetTestVariable(""); + var result = EnvReader.HasValue(TestKey); + result.ShouldBeFalse(); + } + + private static void SetTestVariable(string value) => Environment.SetEnvironmentVariable(TestKey, value); + + private static void ClearTestVariable() => Environment.SetEnvironmentVariable(TestKey, null); +} \ No newline at end of file diff --git a/src/dotenv.net.Tests/WriterTests.cs b/src/dotenv.net.Tests/WriterTests.cs new file mode 100644 index 0000000..645e3c1 --- /dev/null +++ b/src/dotenv.net.Tests/WriterTests.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using Shouldly; +using Xunit; + +namespace dotenv.net.Tests; + +public class WriterTests +{ + private const string TestVariableKey = "TEST_VARIABLE"; + + public WriterTests() => Environment.SetEnvironmentVariable(TestVariableKey, null); + + [Fact] + public void WriteToEnv_WhenOverwriteIsFalseAndVariableDoesNotExist_ShouldSetVariable() + { + var envVars = new Dictionary { { TestVariableKey, "test_value" } }; + + Writer.WriteToEnv(envVars, overwriteExistingVars: false); + + Environment.GetEnvironmentVariable(TestVariableKey).ShouldBe("test_value"); + } + + [Fact] + public void WriteToEnv_WhenOverwriteIsFalseAndVariableExists_ShouldNotChangeVariable() + { + const string existingValue = "existing_value"; + Environment.SetEnvironmentVariable(TestVariableKey, existingValue); + var envVars = new Dictionary { { TestVariableKey, "new_value" } }; + + Writer.WriteToEnv(envVars, overwriteExistingVars: false); + + Environment.GetEnvironmentVariable(TestVariableKey).ShouldBe(existingValue); + } + + [Fact] + public void WriteToEnv_WhenOverwriteIsTrueAndVariableExists_ShouldChangeVariable() + { + const string existingValue = "existing_value"; + const string newValue = "new_value"; + Environment.SetEnvironmentVariable(TestVariableKey, existingValue); + var envVars = new Dictionary { { TestVariableKey, newValue } }; + + Writer.WriteToEnv(envVars, overwriteExistingVars: true); + + Environment.GetEnvironmentVariable(TestVariableKey).ShouldBe(newValue); + } +} \ No newline at end of file diff --git a/src/dotenv.net.Tests/dotenv.net.Tests.csproj b/src/dotenv.net.Tests/dotenv.net.Tests.csproj new file mode 100644 index 0000000..79ff41d --- /dev/null +++ b/src/dotenv.net.Tests/dotenv.net.Tests.csproj @@ -0,0 +1,28 @@ + + + net9.0 + false + latest + enable + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers + all + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + \ No newline at end of file diff --git a/src/dotenv.net/DotEnv.cs b/src/dotenv.net/DotEnv.cs index 52fe65f..d9431c6 100644 --- a/src/dotenv.net/DotEnv.cs +++ b/src/dotenv.net/DotEnv.cs @@ -1,32 +1,10 @@ -using System; -using System.Collections.Generic; -using dotenv.net.Utilities; +using System.Collections.Generic; +using System.Linq; namespace dotenv.net; public static class DotEnv { - /// - /// [Deprecated] Configure the environment variables from a .env file - /// - /// Options on how to load the env file - [Obsolete( - "This method would be removed in the next major release. Use the Fluent API, Load() or Read() methods instead.")] - public static void Config(DotEnvOptions options) => Helpers.ReadAndWrite(options); - - /// - /// [Deprecated] Searches the current directory and three directories up and loads the environment variables - /// - /// The number of top-level directories to search; the default is 4 top-level directories. - /// States whether the operation succeeded - [Obsolete( - "This method would be removed in the next major release. Use the Fluent API, Load() or Read() methods instead.")] - public static bool AutoConfig(int levelsToSearch = DotEnvOptions.DefaultProbeDepth) - { - Helpers.ReadAndWrite(new DotEnvOptions(probeLevelsToSearch: levelsToSearch)); - return true; - } - /// /// Initialize the fluent configuration API /// @@ -37,12 +15,32 @@ public static bool AutoConfig(int levelsToSearch = DotEnvOptions.DefaultProbeDep /// /// The options required to configure the env loader /// The key value pairs read from the env files - public static IDictionary Read(DotEnvOptions? options = null) => - Helpers.ReadAndReturn(options ?? new DotEnvOptions()); + public static IDictionary Read(DotEnvOptions? options = null) + { + options ??= new DotEnvOptions(); + var envFilePaths = options.ProbeForEnv + ? [Reader.GetProbedEnvPath(options.ProbeLevelsToSearch, options.IgnoreExceptions)] + : options.EnvFilePaths; + var envFileKeyValues = envFilePaths + .Select(envFilePath => + { + var fileRows = Reader.ReadFileLines(envFilePath, options.IgnoreExceptions, options.Encoding); + var envKeyValues = Reader.ExtractEnvKeyValues(fileRows, options.TrimValues); + return envKeyValues.ToArray(); + }) + .ToList(); + + return Reader.MergeEnvKeyValues(envFileKeyValues, options.OverwriteExistingVars); + } /// /// Load the values in the provided env files into the environment variables /// /// The options required to configure the env loader - public static void Load(DotEnvOptions? options = null) => Helpers.ReadAndWrite(options ?? new DotEnvOptions()); + public static void Load(DotEnvOptions? options = null) + { + options ??= new DotEnvOptions(); + var envVars = Read(options); + Writer.WriteToEnv(envVars, options.OverwriteExistingVars); + } } diff --git a/src/dotenv.net/DotEnvOptions.cs b/src/dotenv.net/DotEnvOptions.cs index 0841539..e987791 100644 --- a/src/dotenv.net/DotEnvOptions.cs +++ b/src/dotenv.net/DotEnvOptions.cs @@ -6,9 +6,9 @@ namespace dotenv.net; public class DotEnvOptions { - private static readonly string[] DefaultEnvPath = { DefaultEnvFileName }; + private static readonly string[] DefaultEnvPath = [DefaultEnvFileName]; internal const string DefaultEnvFileName = ".env"; - internal const int DefaultProbeDepth = 4; + internal const int DefaultProbeAscendLimit = 4; /// /// A value to state whether to throw or swallow exceptions. The default is true. @@ -31,7 +31,8 @@ public class DotEnvOptions public bool TrimValues { get; private set; } /// - /// A value to state whether to override the env variable if it has been set. the default is true. + /// Whether to overwrite existing environment variables. Also applies to multiple env files; if false and other env + /// files have the same key, the values are ignored. Defaults to true. /// public bool OverwriteExistingVars { get; private set; } @@ -48,16 +49,16 @@ public class DotEnvOptions /// /// Default constructor for the dot env options /// - /// Whether or not to ignore exceptions + /// Whether to ignore exceptions /// The encoding the env files are in - /// Whether or not to trim whitespace from the read values - /// Whether or not to overwrite - /// Whether or not to search up the directories looking for an env file + /// Whether to trim whitespace from the read values + /// Whether to overwrite + /// Whether to search up the directories looking for an env file /// How high up the directory chain to search /// The env file paths to load - public DotEnvOptions(bool ignoreExceptions = true, IEnumerable envFilePaths = null, - Encoding encoding = null, bool trimValues = false, bool overwriteExistingVars = true, - bool probeForEnv = false, int probeLevelsToSearch = DefaultProbeDepth) + public DotEnvOptions(bool ignoreExceptions = true, IEnumerable? envFilePaths = null, + Encoding? encoding = null, bool trimValues = false, bool overwriteExistingVars = true, + bool probeForEnv = false, int probeLevelsToSearch = DefaultProbeAscendLimit) { IgnoreExceptions = ignoreExceptions; EnvFilePaths = envFilePaths?.Any() != true ? DefaultEnvPath : envFilePaths; @@ -65,7 +66,7 @@ public DotEnvOptions(bool ignoreExceptions = true, IEnumerable envFilePa TrimValues = trimValues; OverwriteExistingVars = overwriteExistingVars; ProbeForEnv = probeForEnv; - ProbeLevelsToSearch = probeLevelsToSearch; + ProbeLevelsToSearch = probeLevelsToSearch < 0 ? DefaultProbeAscendLimit : probeLevelsToSearch; } /// @@ -89,24 +90,24 @@ public DotEnvOptions WithoutExceptions() } /// - /// Search up the directory for a .env file. By default searches up 4 directories. + /// Search up the directory for a .env file. Searches up 4 directory levels by default. /// /// configured dot env options - public DotEnvOptions WithProbeForEnv(int probeLevelsToSearch = DefaultProbeDepth) + public DotEnvOptions WithProbeForEnv(int probeLevelsToSearch = DefaultProbeAscendLimit) { ProbeForEnv = true; - ProbeLevelsToSearch = probeLevelsToSearch; + ProbeLevelsToSearch = probeLevelsToSearch < 0 ? DefaultProbeAscendLimit : probeLevelsToSearch; return this; } /// - /// Rely on the provided env files. By default is false. + /// Rely on the provided env files. Defaults to false. /// /// configured dot env options public DotEnvOptions WithoutProbeForEnv() { ProbeForEnv = false; - ProbeLevelsToSearch = DefaultProbeDepth; + ProbeLevelsToSearch = DefaultProbeAscendLimit; return this; } @@ -187,7 +188,7 @@ public DotEnvOptions WithEnvFiles(params string[] envFilePaths) public IDictionary Read() => DotEnv.Read(this); /// - /// Read the env files and write to the system environment variables + /// ReadFileLines the env files and write to the system environment variables /// public void Load() => DotEnv.Load(this); } diff --git a/src/dotenv.net/Parser.cs b/src/dotenv.net/Parser.cs index acaeb4d..bfc742c 100644 --- a/src/dotenv.net/Parser.cs +++ b/src/dotenv.net/Parser.cs @@ -1,17 +1,14 @@ using System; using System.Collections.Generic; using System.Text; -using System.Text.RegularExpressions; namespace dotenv.net; internal static class Parser { - private const string SingleQuote = "'"; - private const string DoubleQuotes = "\""; - - private static readonly Regex IsQuotedLineStart = new("^[a-zA-Z0-9_ .-]+=\\s*\".*$", RegexOptions.Compiled); - private static readonly Regex IsQuotedLineEnd = new("(?> Parse(ReadOnlySpan rawEnvRows, bool trimValues) @@ -32,35 +29,17 @@ internal static ReadOnlySpan> Parse(ReadOnlySpan= rawEnvRows.Length) - break; - - rawEnvRow = rawEnvRows[i]; - valueBuilder.AppendLine(); - valueBuilder.Append(rawEnvRow); - } - - value = valueBuilder.ToString(); - } - else - { - value = rawValue; - } - - value = StripQuotes(value); + var value = isSingleQuoted || isDoubleQuoted + ? ParseQuotedValue(key, rawEnvRows, trimmedRawValue, ref i) + : rawValue; if (trimValues) value = value.Trim(); @@ -71,25 +50,69 @@ internal static ReadOnlySpan> Parse(ReadOnlySpan value.StartsWith("#"); - - private static string StripQuotes(this string value) + private static string ParseQuotedValue(string key, ReadOnlySpan rawEnvRows, string currentRowValue, + ref int i) { - var trimmed = value.Trim(); - var modified = false; + var quoteChar = currentRowValue[0]; + var valueBuilder = new StringBuilder(); + var currentLineContent = currentRowValue.Substring(1); // Start after the opening quote. - if (trimmed.Length > 1 && - ((trimmed.StartsWith(DoubleQuotes) && trimmed.EndsWith(DoubleQuotes)) || - (trimmed.StartsWith(SingleQuote) && trimmed.EndsWith(SingleQuote)))) + while (true) { - trimmed = trimmed.Substring(1, trimmed.Length - 2); - modified = true; - } + var endQuoteIndex = -1; + var searchFrom = 0; + + // find the next unescaped quote on the current line. + while (searchFrom < currentLineContent.Length) + { + var nextQuote = currentLineContent.IndexOf(quoteChar, searchFrom); + + // no more quotes on this line + if (nextQuote == -1) + break; + // count preceding backslashes to see if the quote is escaped. + var backslashCount = 0; + for (var j = nextQuote - 1; j >= 0 && currentLineContent[j] == BackSlash; j--) + backslashCount++; - return modified ? trimmed : value; + // an even number of backslashes means the quote is NOT escaped. + if (backslashCount % 2 == 0) + { + endQuoteIndex = nextQuote; + break; + } + + // odd number of backslashes means it's escaped, continue searching + searchFrom = nextQuote + 1; + } + + if (endQuoteIndex != -1) + { + // closing quote found. Append the content before it and exit + valueBuilder.Append(currentLineContent, 0, endQuoteIndex); + break; + } + + // no closing quote on this line, append the whole line and move to the next + valueBuilder.Append(currentLineContent); + i++; + + if (i >= rawEnvRows.Length) + throw new ArgumentException( + $"Unable to parse environment variable: {key}. Missing closing quote."); + + valueBuilder.AppendLine(); + currentLineContent = rawEnvRows[i]; + } + + return valueBuilder.ToString() + .UnescapeQuotes(quoteChar) + .UnescapeBackslashes(); } + private static bool IsComment(this string value) => value.StartsWith("#"); + private static bool HasKey(this string value, out int index) { index = value.IndexOf('='); @@ -102,4 +125,12 @@ private static (string Key, string Value) SplitIntoKv(this string rawEnvRow, int var value = rawEnvRow.Substring(index + 1); return (key, value); } + + private static bool StartsWith(this string input, char character) => + !string.IsNullOrEmpty(input) && input[0] == character; + + private static string UnescapeQuotes(this string input, char quoteChar) => + input.Replace($"\\{quoteChar}", quoteChar.ToString()); + + private static string UnescapeBackslashes(this string input) => input.Replace("\\\\", "\\"); } diff --git a/src/dotenv.net/README.md b/src/dotenv.net/README.md deleted file mode 100644 index 41a8430..0000000 --- a/src/dotenv.net/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# dotenv.net - -[![NuGet Badge](https://buildstats.info/nuget/dotenv.net)](https://www.nuget.org/packages/dotenv.net) - -dotenv.net is a zero-dependency module that loads environment variables from a .env environment variable file into `System.Environment`. - -## Usage - -### Conventional - -First install the library as a dependency in your application from nuget - -``` -Install-Package dotenv.net -``` - -or - -``` -dotnet add package dotenv.net -``` - -or for paket - -``` -paket add dotenv.net -``` - -Create a file with no filename and an extension of `.env`. - -A sample `.env` file would look like this: -```text -DB_HOST=localhost -DB_USER=root -DB_PASS=s1mpl3 -``` - -in the `Startup.cs` file or as early as possible in your code add the following: - -```csharp -using dotenv.net; - -... - -DotEnv.Config(); -``` - -At times the `.env` would not be in the same directory as your assembly, in that case the following would apply - -```csharp -using dotenv.net - -... - -var success = DotEnv.AutoConfig(); -if(success) -{ - // env successfully found and read -} -``` - -this looks through your assembly's folder and three folders up for a `.env` file and loads that. - -the values saved in your `.env` file would be available in your application and can be accessed via: - ```csharp -Environment.GetEnvironmentVariable("DB_HOST"); // would output 'localhost' -``` \ No newline at end of file diff --git a/src/dotenv.net/Reader.cs b/src/dotenv.net/Reader.cs index 27b7f3d..a5e4ec0 100644 --- a/src/dotenv.net/Reader.cs +++ b/src/dotenv.net/Reader.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Text; @@ -6,7 +7,7 @@ namespace dotenv.net; internal static class Reader { - internal static ReadOnlySpan Read(string envFilePath, bool ignoreExceptions, Encoding? encoding) + internal static ReadOnlySpan ReadFileLines(string envFilePath, bool ignoreExceptions, Encoding? encoding) { var defaultResponse = ReadOnlySpan.Empty; @@ -34,4 +35,54 @@ internal static ReadOnlySpan Read(string envFilePath, bool ignoreExcepti // read all lines from the env file return new ReadOnlySpan(File.ReadAllLines(envFilePath, encoding)); } + + internal static ReadOnlySpan> ExtractEnvKeyValues(ReadOnlySpan rawEnvRows, + bool trimValues) => rawEnvRows == ReadOnlySpan.Empty + ? ReadOnlySpan>.Empty + : Parser.Parse(rawEnvRows, trimValues); + + internal static Dictionary MergeEnvKeyValues( + IEnumerable[]> envFileKeyValues, bool overwriteExistingVars) + { + var response = new Dictionary(); + + foreach (var envFileKeyValue in envFileKeyValues) + foreach (var envKeyValue in envFileKeyValue) + // if the key does not exist or if a previous env file has the same key, and we are allowed to overwrite it + if (!response.ContainsKey(envKeyValue.Key) || + (response.ContainsKey(envKeyValue.Key) && overwriteExistingVars)) + response[envKeyValue.Key] = envKeyValue.Value; + + return response; + } + + internal static string GetProbedEnvPath(int levelsToSearch, bool ignoreExceptions) + { + var currentDirectory = new DirectoryInfo(AppContext.BaseDirectory); + var count = levelsToSearch; + var foundEnvPath = SearchPaths(); + + if (string.IsNullOrEmpty(foundEnvPath) && !ignoreExceptions) + throw new FileNotFoundException( + $"Failed to find a file matching the '{DotEnvOptions.DefaultEnvFileName}' search pattern." + + $"{Environment.NewLine}Current Directory: {currentDirectory}" + + $"{Environment.NewLine}Levels Searched: {levelsToSearch}"); + + return foundEnvPath; + + + string? SearchPaths() + { + for (; + currentDirectory != null && count > 0; + count--, currentDirectory = currentDirectory.Parent + ) + foreach (var file in currentDirectory.GetFiles( + DotEnvOptions.DefaultEnvFileName, SearchOption.TopDirectoryOnly) + ) + return file.FullName; + + return null; + } + } } diff --git a/src/dotenv.net/Utilities/EnvReader.cs b/src/dotenv.net/Utilities/EnvReader.cs index efbf3b6..06f22ed 100644 --- a/src/dotenv.net/Utilities/EnvReader.cs +++ b/src/dotenv.net/Utilities/EnvReader.cs @@ -15,13 +15,11 @@ public static class EnvReader /// When the value could not be found public static string GetStringValue(string key) { - if (TryGetStringValue(key, out var value)) - { - return value; - } + if (TryGetStringValue(key, out var value)) + return value!; - throw new Exception("Value could not be retrieved."); - } + throw new Exception("Value could not be retrieved."); + } /// /// Retrieve an integer value from the current environment @@ -31,13 +29,11 @@ public static string GetStringValue(string key) /// When the value could not be found or is not an integer public static int GetIntValue(string key) { - if (TryGetIntValue(key, out var value)) - { - return value; - } + if (TryGetIntValue(key, out var value)) + return value; - throw new Exception("Value could not be retrieved."); - } + throw new Exception("Value could not be retrieved."); + } /// /// Retrieve a double value from the current environment @@ -47,13 +43,11 @@ public static int GetIntValue(string key) /// When the value could not be found or is not a valid double public static double GetDoubleValue(string key) { - if (TryGetDoubleValue(key, out var value)) - { - return value; - } + if (TryGetDoubleValue(key, out var value)) + return value; - throw new Exception("Value could not be retrieved."); - } + throw new Exception("Value could not be retrieved."); + } /// /// Retrieve a decimal value from the current environment @@ -63,13 +57,11 @@ public static double GetDoubleValue(string key) /// When the value could not be found or is not a valid decimal public static decimal GetDecimalValue(string key) { - if (TryGetDecimalValue(key, out var value)) - { - return value; - } + if (TryGetDecimalValue(key, out var value)) + return value; - throw new Exception("Value could not be retrieved."); - } + throw new Exception("Value could not be retrieved."); + } /// /// Retrieve a boolean value from the current environment @@ -79,13 +71,11 @@ public static decimal GetDecimalValue(string key) /// When the value could not be found or is not a valid bool public static bool GetBooleanValue(string key) { - if (TryGetBooleanValue(key, out var value)) - { - return value; - } + if (TryGetBooleanValue(key, out var value)) + return value; - throw new Exception("Value could not be retrieved."); - } + throw new Exception("Value could not be retrieved."); + } /// /// Try to retrieve a value from the current environment @@ -93,20 +83,20 @@ public static bool GetBooleanValue(string key) /// The key to retrieve the value via /// The string value retrieved or null /// A value representing the retrieval success status - public static bool TryGetStringValue(string key, out string value) + public static bool TryGetStringValue(string key, out string? value) { - var retrievedValue = Environment.GetEnvironmentVariable(key); - - if (!string.IsNullOrEmpty(retrievedValue)) - { - value = retrievedValue; - return true; - } + var retrievedValue = Environment.GetEnvironmentVariable(key); - value = null; - return false; + if (!string.IsNullOrEmpty(retrievedValue)) + { + value = retrievedValue; + return true; } + value = null; + return false; + } + /// /// Try to retrieve an int value from the current environment /// @@ -115,16 +105,14 @@ public static bool TryGetStringValue(string key, out string value) /// A value representing the retrieval success status public static bool TryGetIntValue(string key, out int value) { - var retrievedValue = Environment.GetEnvironmentVariable(key); + var retrievedValue = Environment.GetEnvironmentVariable(key); - if (!string.IsNullOrEmpty(retrievedValue)) - { - return int.TryParse(retrievedValue, out value); - } + if (!string.IsNullOrEmpty(retrievedValue)) + return int.TryParse(retrievedValue, out value); - value = 0; - return false; - } + value = 0; + return false; + } /// /// Try to retrieve a double value from the current environment @@ -134,16 +122,14 @@ public static bool TryGetIntValue(string key, out int value) /// A value representing the retrieval success status public static bool TryGetDoubleValue(string key, out double value) { - var retrievedValue = Environment.GetEnvironmentVariable(key); + var retrievedValue = Environment.GetEnvironmentVariable(key); - if (!string.IsNullOrEmpty(retrievedValue)) - { - return double.TryParse(retrievedValue, out value); - } + if (!string.IsNullOrEmpty(retrievedValue)) + return double.TryParse(retrievedValue, out value); - value = 0.0; - return false; - } + value = 0.0; + return false; + } /// /// Try to retrieve a decimal value from the current environment @@ -153,16 +139,14 @@ public static bool TryGetDoubleValue(string key, out double value) /// A value representing the retrieval success status public static bool TryGetDecimalValue(string key, out decimal value) { - var retrievedValue = Environment.GetEnvironmentVariable(key); + var retrievedValue = Environment.GetEnvironmentVariable(key); - if (!string.IsNullOrEmpty(retrievedValue)) - { - return decimal.TryParse(retrievedValue, out value); - } + if (!string.IsNullOrEmpty(retrievedValue)) + return decimal.TryParse(retrievedValue, out value); - value = 0.0m; - return false; - } + value = 0.0m; + return false; + } /// /// Try to retrieve a boolean value from the current environment @@ -172,16 +156,14 @@ public static bool TryGetDecimalValue(string key, out decimal value) /// A value representing the retrieval success status public static bool TryGetBooleanValue(string key, out bool value) { - var retrievedValue = Environment.GetEnvironmentVariable(key); + var retrievedValue = Environment.GetEnvironmentVariable(key); - if (!string.IsNullOrEmpty(retrievedValue)) - { - return bool.TryParse(retrievedValue, out value); - } + if (!string.IsNullOrEmpty(retrievedValue)) + return bool.TryParse(retrievedValue, out value); - value = false; - return false; - } + value = false; + return false; + } /// /// Determine if an environment key has a set value or not @@ -190,7 +172,7 @@ public static bool TryGetBooleanValue(string key, out bool value) /// A value determining if a value is set or not public static bool HasValue(string key) { - var retrievedValue = Environment.GetEnvironmentVariable(key); - return !string.IsNullOrEmpty(retrievedValue); - } -} \ No newline at end of file + var retrievedValue = Environment.GetEnvironmentVariable(key); + return !string.IsNullOrEmpty(retrievedValue); + } +} diff --git a/src/dotenv.net/Utilities/Helpers.cs b/src/dotenv.net/Utilities/Helpers.cs deleted file mode 100644 index 96dfc3e..0000000 --- a/src/dotenv.net/Utilities/Helpers.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace dotenv.net.Utilities; - -internal static class Helpers -{ - private static ReadOnlySpan> ReadAndParse(string envFilePath, - bool ignoreExceptions, Encoding encoding, bool trimValues) - { - var rawEnvRows = Reader.Read(envFilePath, ignoreExceptions, encoding); - - return rawEnvRows == ReadOnlySpan.Empty - ? ReadOnlySpan>.Empty - : Parser.Parse(rawEnvRows, trimValues); - } - - internal static IDictionary ReadAndReturn(DotEnvOptions options) - { - var response = new Dictionary(); - var envFilePaths = options.ProbeForEnv - ? new[] { GetProbedEnvPath(options.ProbeLevelsToSearch, options.IgnoreExceptions) } - : options.EnvFilePaths; - - foreach (var envFilePath in envFilePaths) - { - var envRows = ReadAndParse(envFilePath, options.IgnoreExceptions, options.Encoding, - options.TrimValues); - - foreach (var envRow in envRows) - response[envRow.Key] = envRow.Value; - } - - return response; - } - - internal static void ReadAndWrite(DotEnvOptions options) - { - var envVars = ReadAndReturn(options); - - foreach (var envVar in envVars) - { - if (options.OverwriteExistingVars) - Environment.SetEnvironmentVariable(envVar.Key, envVar.Value); - else if (!EnvReader.HasValue(envVar.Key)) - Environment.SetEnvironmentVariable(envVar.Key, envVar.Value); - } - } - - private static string GetProbedEnvPath(int levelsToSearch, bool ignoreExceptions) - { - var currentDirectory = new DirectoryInfo(AppContext.BaseDirectory); - var count = levelsToSearch; - var foundEnvPath = SearchPaths(); - - if (string.IsNullOrEmpty(foundEnvPath) && !ignoreExceptions) - { - throw new FileNotFoundException( - $"Failed to find a file matching the '{DotEnvOptions.DefaultEnvFileName}' search pattern." + - $"{Environment.NewLine}Current Directory: {currentDirectory}" + - $"{Environment.NewLine}Levels Searched: {levelsToSearch}"); - } - - return foundEnvPath; - - - string SearchPaths() - { - for (; - currentDirectory != null && count > 0; - count--, currentDirectory = currentDirectory.Parent - ) - { - foreach (var file in currentDirectory.GetFiles( - DotEnvOptions.DefaultEnvFileName, SearchOption.TopDirectoryOnly) - ) - return file.FullName; - } - - return null; - } - } -} diff --git a/src/dotenv.net/Writer.cs b/src/dotenv.net/Writer.cs new file mode 100644 index 0000000..951d246 --- /dev/null +++ b/src/dotenv.net/Writer.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using dotenv.net.Utilities; + +namespace dotenv.net; + +internal static class Writer +{ + public static void WriteToEnv(IDictionary envVars, bool overwriteExistingVars) + { + foreach (var envVar in envVars) + if (overwriteExistingVars || !EnvReader.HasValue(envVar.Key)) + Environment.SetEnvironmentVariable(envVar.Key, envVar.Value); + } +} diff --git a/src/dotenv.net/dotenv.net.csproj b/src/dotenv.net/dotenv.net.csproj index f7ad23a..4e6aa51 100644 --- a/src/dotenv.net/dotenv.net.csproj +++ b/src/dotenv.net/dotenv.net.csproj @@ -1,39 +1,34 @@  - - ©2017-2024 , Bolorunduro Winner-Timothy. All Rights reserved. - - add multiline env var support - Winner-Timothy Bolorunduro - true - dotenv.net - A library to read variables from a .env file and populate the environment variables. - https://res.cloudinary.com/dg2dgzbt4/image/upload/v1587070177/external_assets/open_source/icons/dotenv.png - https://github.com/bolorundurowb/dotenv.net - https://github.com/bolorundurowb/dotenv.net/blob/master/LICENSE - https://github.com/bolorundurowb/dotenv.net - default - README.md - 3.2.1 - git - dotnet, environment, variables, env, dotenv, net core, autofac - true - 3.2.1 - 3.2.1 - en-NG - netstandard1.6;netstandard2.0;netstandard2.1 - dotenv.net - enable - + + ©2017-2025 , Bolorunduro Winner-Timothy. All Rights reserved. + - support quotation escape support + Winner-Timothy Bolorunduro + true + dotenv.net + A library to read variables from a .env file and populate the environment variables. + https://res.cloudinary.com/dg2dgzbt4/image/upload/v1587070177/external_assets/open_source/icons/dotenv.png + https://github.com/bolorundurowb/dotenv.net + https://github.com/bolorundurowb/dotenv.net/blob/master/LICENSE + https://github.com/bolorundurowb/dotenv.net + default + README.md + 4.0.0 + git + dotnet, environment, variables, env, dotenv, net core, autofac + true + en-NG + netstandard2.0;netstandard2.1 + dotenv.net + enable + true + - - - + + + + - - - - - - - - + + + diff --git a/tests/dotenv.net.Tests/.env b/tests/dotenv.net.Tests/.env deleted file mode 100644 index 367b05c..0000000 --- a/tests/dotenv.net.Tests/.env +++ /dev/null @@ -1,4 +0,0 @@ -hello=world -# comment -strongestavenger = -uniquekey=kjdjkd \ No newline at end of file diff --git a/tests/dotenv.net.Tests/DotEnv.Fluent.Tests.cs b/tests/dotenv.net.Tests/DotEnv.Fluent.Tests.cs deleted file mode 100644 index 3507186..0000000 --- a/tests/dotenv.net.Tests/DotEnv.Fluent.Tests.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System; -using System.IO; -using System.Text; -using dotenv.net.Utilities; -using FluentAssertions; -using Xunit; - -namespace dotenv.net.Tests; - -public class DotEnvFluentTests -{ - private const string WhitespacesEnvFileName = "whitespaces.env"; - private const string NonExistentEnvFileName = "non-existent.env"; - private const string QuotationsEnvFileName = "quotations.env"; - private const string MultiLinesEnvFileName = "multi-lines.env"; - private const string AsciiEnvFileName = "ascii.env"; - private const string GenericEnvFileName = "generic.env"; - private const string IncompleteEnvFileName = "incomplete.env"; - - [Fact] - public void ConfigShouldThrowWithNonExistentEnvAndTrackedExceptions() - { - var action = new Action(() => DotEnv.Fluent() - .WithExceptions() - .WithEnvFiles(NonExistentEnvFileName) - .Load()); - - action.Should() - .ThrowExactly(); - } - - [Fact] - public void ConfigShouldLoadEnvWithProvidedEncoding() - { - DotEnv.Fluent() - .WithEncoding(Encoding.ASCII) - .WithEnvFiles(AsciiEnvFileName) - .Load(); - - EnvReader.GetStringValue("ENCODING") - .Should() - .Be("ASCII"); - } - - [Fact] - public void ConfigShouldLoadEnvWithTrimOptions() - { - DotEnv.Fluent() - .WithEnvFiles(WhitespacesEnvFileName) - .WithTrimValues() - .Load(); - - EnvReader.GetStringValue("DB_DATABASE") - .Should() - .Be("laravel"); - - DotEnv.Fluent() - .WithEnvFiles(WhitespacesEnvFileName) - .WithoutTrimValues() - .Load(); - - EnvReader.GetStringValue("DB_DATABASE") - .Should() - .Be(" laravel "); - } - - [Fact] - public void ConfigShouldLoadEnvWithExistingVarOverwriteOptions() - { - Environment.SetEnvironmentVariable("Generic", "Existing"); - - DotEnv.Fluent() - .WithEnvFiles(GenericEnvFileName) - .WithoutOverwriteExistingVars() - .Load(); - - EnvReader.GetStringValue("Generic") - .Should() - .Be("Existing"); - - DotEnv.Fluent() - .WithEnvFiles(GenericEnvFileName) - .WithOverwriteExistingVars() - .Load(); - - EnvReader.GetStringValue("Generic") - .Should() - .Be("Value"); - } - - [Fact] - public void ConfigShouldLoadDefaultEnvWithProbeOptions() - { - var action = new Action(() => DotEnv.Fluent() - .WithProbeForEnv(2) - .WithExceptions() - .Load()); - - action.Should() - .ThrowExactly(); - - action = () => DotEnv.Fluent() - .WithProbeForEnv(5) - .WithExceptions() - .Load(); - - action.Should() - .NotThrow(); - - EnvReader.GetStringValue("hello") - .Should() - .Be("world"); - } - - [Fact] - public void ConfigShouldLoadEnvWithQuotedValues() - { - DotEnv.Fluent() - .WithEnvFiles(QuotationsEnvFileName) - .WithTrimValues() - .Load(); - - EnvReader.GetStringValue("DOUBLE_QUOTES") - .Should() - .Be("double"); - EnvReader.GetStringValue("SINGLE_QUOTES") - .Should() - .Be("single"); - } - - [Fact] - public void ConfigLoadsMultilineEnvs() - { - DotEnv.Fluent() - .WithEnvFiles(MultiLinesEnvFileName) - .WithTrimValues() - .Load(); - - EnvReader.GetStringValue("DOUBLE_QUOTE") - .Should() - .Be("double"); - EnvReader.GetStringValue("DOUBLE_QUOTE_MULTI_LINE") - .Should() - .Be($"dou{Environment.NewLine}bler"); - EnvReader.GetStringValue("DOUBLE_QUOTE_EVEN_MORE_LINES") - .Should() - .Be($"dou{Environment.NewLine}{Environment.NewLine}b{Environment.NewLine}{Environment.NewLine}lest"); - EnvReader.GetStringValue("DOUBLE_QUOTE_WHITESPACE_LINES") - .Should() - .Be($"dou{Environment.NewLine}{Environment.NewLine}b{Environment.NewLine}{Environment.NewLine}lest"); - EnvReader.GetStringValue("DOUBLE_QUOTE.WITH_DOTS") - .Should() - .Be($"dou{Environment.NewLine}{Environment.NewLine}b{Environment.NewLine}{Environment.NewLine}le{Environment.NewLine}dots"); - EnvReader.GetStringValue("DOUBLE_QUOTE_NO_CLOSE") - .Should() - .Be($"\"dou{Environment.NewLine}{Environment.NewLine}b{Environment.NewLine}{Environment.NewLine}lest the more"); - } - - [Fact] - public void ConfigShouldLoadEnvWithInvalidEnvEntries() - { - DotEnv.Fluent() - .WithEnvFiles(IncompleteEnvFileName) - .WithoutTrimValues() - .Load(); - - EnvReader.HasValue("KeyWithNoValue") - .Should() - .BeFalse(); - } -} diff --git a/tests/dotenv.net.Tests/DotEnv.Tests.cs b/tests/dotenv.net.Tests/DotEnv.Tests.cs deleted file mode 100644 index bcd6fda..0000000 --- a/tests/dotenv.net.Tests/DotEnv.Tests.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.IO; -using System.Text; -using dotenv.net.Utilities; -using FluentAssertions; -using Xunit; - -namespace dotenv.net.Tests; - -public class DotEnvTests -{ - private const string WhitespacesEnvFileName = "whitespaces.env"; - private const string NonExistentEnvFileName = "non-existent.env"; - private const string QuotationsEnvFileName = "quotations.env"; - private const string AsciiEnvFileName = "ascii.env"; - private const string GenericEnvFileName = "generic.env"; - private const string IncompleteEnvFileName = "incomplete.env"; - - [Fact] - public void ConfigShouldThrowWithNonExistentEnvAndTrackedExceptions() - { - var action = new Action(() => DotEnv.Config(new DotEnvOptions(ignoreExceptions: false, envFilePaths: new[] { NonExistentEnvFileName }))); - - action.Should() - .ThrowExactly(); - } - - [Fact] - public void ConfigShouldLoadEnvWithProvidedEncoding() - { - DotEnv.Config(new DotEnvOptions(envFilePaths: new[] { AsciiEnvFileName }, encoding: Encoding.ASCII)); - - EnvReader.GetStringValue("ENCODING") - .Should() - .Be("ASCII"); - } - - [Fact] - public void ConfigShouldLoadEnvWithTrimOptions() - { - DotEnv.Config(new DotEnvOptions(envFilePaths: new[] { WhitespacesEnvFileName }, trimValues: true)); - - EnvReader.GetStringValue("DB_DATABASE") - .Should() - .Be("laravel"); - - DotEnv.Config(new DotEnvOptions(envFilePaths: new[] { WhitespacesEnvFileName }, trimValues: false)); - - EnvReader.GetStringValue("DB_DATABASE") - .Should() - .Be(" laravel "); - } - - [Fact] - public void ConfigShouldLoadEnvWithExistingVarOverwriteOptions() - { - Environment.SetEnvironmentVariable("Generic", "Existing"); - - DotEnv.Config(new DotEnvOptions(envFilePaths: new[] { GenericEnvFileName }, overwriteExistingVars: false)); - - EnvReader.GetStringValue("Generic") - .Should() - .Be("Existing"); - - DotEnv.Config(new DotEnvOptions(envFilePaths: new[] { GenericEnvFileName }, overwriteExistingVars: true)); - - EnvReader.GetStringValue("Generic") - .Should() - .Be("Value"); - } - - [Fact] - public void ConfigShouldLoadDefaultEnvWithProbeOptions() - { - var action = new Action(() => DotEnv.Config(new DotEnvOptions(probeForEnv: true, probeLevelsToSearch: 2, ignoreExceptions: false))); - - action.Should() - .ThrowExactly(); - - action = () => DotEnv.Config(new DotEnvOptions(probeForEnv: true, probeLevelsToSearch: 5, ignoreExceptions: false)); - - action.Should() - .NotThrow(); - - EnvReader.GetStringValue("hello") - .Should() - .Be("world"); - } - - [Fact] - public void ConfigShouldLoadEnvWithQuotedValues() - { - DotEnv.Config(new DotEnvOptions(envFilePaths: new[] { QuotationsEnvFileName }, trimValues: true)); - - EnvReader.GetStringValue("DOUBLE_QUOTES") - .Should() - .Be("double"); - EnvReader.GetStringValue("SINGLE_QUOTES") - .Should() - .Be("single"); - } - - [Fact] - public void ConfigShouldLoadEnvWithInvalidEnvEntries() - { - DotEnv.Config(new DotEnvOptions(envFilePaths: new[] { IncompleteEnvFileName }, trimValues: false)); - - EnvReader.HasValue("KeyWithNoValue") - .Should() - .BeFalse(); - } - - [Fact] - public void AutoConfigShouldLoadDefaultEnvWithProbeOptions() - { - Action action = () => DotEnv.AutoConfig(5); - - action.Should() - .NotThrow(); - - EnvReader.GetStringValue("hello") - .Should() - .Be("world"); - } -} \ No newline at end of file diff --git a/tests/dotenv.net.Tests/DotEnvOptions.Tests.cs b/tests/dotenv.net.Tests/DotEnvOptions.Tests.cs deleted file mode 100644 index 1b4452a..0000000 --- a/tests/dotenv.net.Tests/DotEnvOptions.Tests.cs +++ /dev/null @@ -1,412 +0,0 @@ -using System; -using System.Text; -using FluentAssertions; -using Xunit; - -namespace dotenv.net.Tests; - -public class DotEnvOptionsTests -{ - [Fact] - public void ConstructorShouldInitializeWithDefaults() - { - var options = new DotEnvOptions(); - - options.IgnoreExceptions - .Should() - .BeTrue(); - options.Encoding - .Should() - .Be(Encoding.UTF8); - options.TrimValues - .Should() - .BeFalse(); - options.OverwriteExistingVars - .Should() - .BeTrue(); - options.ProbeForEnv - .Should() - .BeFalse(); - options.ProbeLevelsToSearch - .Should() - .Be(4); - options.EnvFilePaths - .Should() - .Contain(".env"); - } - - [Fact] - public void ConstructorShouldInitializeWithSpecifiedValues() - { - var filePaths = new[] { "test.env" }; - var options = new DotEnvOptions(encoding: Encoding.UTF32, trimValues: false, probeForEnv: true, - probeLevelsToSearch: 5, overwriteExistingVars: false, ignoreExceptions: false, envFilePaths: filePaths); - - options.IgnoreExceptions - .Should() - .BeFalse(); - options.Encoding - .Should() - .Be(Encoding.UTF32); - options.TrimValues - .Should() - .BeFalse(); - options.OverwriteExistingVars - .Should() - .BeFalse(); - options.ProbeForEnv - .Should() - .BeTrue(); - options.ProbeLevelsToSearch - .Should() - .Be(5); - options.EnvFilePaths - .Should() - .Contain("test.env"); - } - - [Fact] - public void ShouldGenerateOptionsWithExceptions() - { - var options = new DotEnvOptions() - .WithExceptions(); - - options.IgnoreExceptions - .Should() - .BeFalse(); - options.Encoding - .Should() - .Be(Encoding.UTF8); - options.TrimValues - .Should() - .BeFalse(); - options.OverwriteExistingVars - .Should() - .BeTrue(); - options.ProbeForEnv - .Should() - .BeFalse(); - options.ProbeLevelsToSearch - .Should() - .Be(4); - options.EnvFilePaths - .Should() - .Contain(".env"); - } - - [Fact] - public void ShouldGenerateOptionsWithoutExceptions() - { - var options = new DotEnvOptions() - .WithoutExceptions(); - - options.IgnoreExceptions - .Should() - .BeTrue(); - options.Encoding - .Should() - .Be(Encoding.UTF8); - options.TrimValues - .Should() - .BeFalse(); - options.OverwriteExistingVars - .Should() - .BeTrue(); - options.ProbeForEnv - .Should() - .BeFalse(); - options.ProbeLevelsToSearch - .Should() - .Be(4); - options.EnvFilePaths - .Should() - .Contain(".env"); - } - - [Fact] - public void ShouldGenerateOptionsWithProbeForEnv() - { - var options = new DotEnvOptions() - .WithProbeForEnv(7); - - options.IgnoreExceptions - .Should() - .BeTrue(); - options.Encoding - .Should() - .Be(Encoding.UTF8); - options.TrimValues - .Should() - .BeFalse(); - options.OverwriteExistingVars - .Should() - .BeTrue(); - options.ProbeForEnv - .Should() - .BeTrue(); - options.ProbeLevelsToSearch - .Should() - .Be(7); - options.EnvFilePaths - .Should() - .Contain(".env"); - } - - [Fact] - public void ShouldGenerateOptionsWithoutProbeForEnv() - { - var options = new DotEnvOptions() - .WithoutProbeForEnv(); - - options.IgnoreExceptions - .Should() - .BeTrue(); - options.Encoding - .Should() - .Be(Encoding.UTF8); - options.TrimValues - .Should() - .BeFalse(); - options.OverwriteExistingVars - .Should() - .BeTrue(); - options.ProbeForEnv - .Should() - .BeFalse(); - options.ProbeLevelsToSearch - .Should() - .Be(4); - options.EnvFilePaths - .Should() - .Contain(".env"); - } - - [Fact] - public void ShouldGenerateOptionsWithOverwriteExistingVars() - { - var options = new DotEnvOptions() - .WithOverwriteExistingVars(); - - options.IgnoreExceptions - .Should() - .BeTrue(); - options.Encoding - .Should() - .Be(Encoding.UTF8); - options.TrimValues - .Should() - .BeFalse(); - options.OverwriteExistingVars - .Should() - .BeTrue(); - options.ProbeForEnv - .Should() - .BeFalse(); - options.ProbeLevelsToSearch - .Should() - .Be(4); - options.EnvFilePaths - .Should() - .Contain(".env"); - } - - [Fact] - public void ShouldGenerateOptionsWithoutOverwriteExistingVars() - { - var options = new DotEnvOptions() - .WithoutOverwriteExistingVars(); - - options.IgnoreExceptions - .Should() - .BeTrue(); - options.Encoding - .Should() - .Be(Encoding.UTF8); - options.TrimValues - .Should() - .BeFalse(); - options.OverwriteExistingVars - .Should() - .BeFalse(); - options.ProbeForEnv - .Should() - .BeFalse(); - options.ProbeLevelsToSearch - .Should() - .Be(4); - options.EnvFilePaths - .Should() - .Contain(".env"); - } - - [Fact] - public void ShouldGenerateOptionsWithTrimValues() - { - var options = new DotEnvOptions() - .WithTrimValues(); - - options.IgnoreExceptions - .Should() - .BeTrue(); - options.Encoding - .Should() - .Be(Encoding.UTF8); - options.TrimValues - .Should() - .BeTrue(); - options.OverwriteExistingVars - .Should() - .BeTrue(); - options.ProbeForEnv - .Should() - .BeFalse(); - options.ProbeLevelsToSearch - .Should() - .Be(4); - options.EnvFilePaths - .Should() - .Contain(".env"); - } - - [Fact] - public void ShouldGenerateOptionsWithoutTrimValues() - { - var options = new DotEnvOptions() - .WithoutTrimValues(); - - options.IgnoreExceptions - .Should() - .BeTrue(); - options.Encoding - .Should() - .Be(Encoding.UTF8); - options.TrimValues - .Should() - .BeFalse(); - options.OverwriteExistingVars - .Should() - .BeTrue(); - options.ProbeForEnv - .Should() - .BeFalse(); - options.ProbeLevelsToSearch - .Should() - .Be(4); - options.EnvFilePaths - .Should() - .Contain(".env"); - } - - [Fact] - public void ShouldGenerateOptionsWithEncoding() - { - var options = new DotEnvOptions() - .WithEncoding(Encoding.Latin1); - - options.IgnoreExceptions - .Should() - .BeTrue(); - options.Encoding - .Should() - .Be(Encoding.Latin1); - options.TrimValues - .Should() - .BeFalse(); - options.OverwriteExistingVars - .Should() - .BeTrue(); - options.ProbeForEnv - .Should() - .BeFalse(); - options.ProbeLevelsToSearch - .Should() - .Be(4); - options.EnvFilePaths - .Should() - .Contain(".env"); - } - - [Fact] - public void ShouldGenerateOptionsWithDefaultEncoding() - { - var options = new DotEnvOptions() - .WithDefaultEncoding(); - - options.IgnoreExceptions - .Should() - .BeTrue(); - options.Encoding - .Should() - .Be(Encoding.UTF8); - options.TrimValues - .Should() - .BeFalse(); - options.OverwriteExistingVars - .Should() - .BeTrue(); - options.ProbeForEnv - .Should() - .BeFalse(); - options.ProbeLevelsToSearch - .Should() - .Be(4); - options.EnvFilePaths - .Should() - .Contain(".env"); - } - - [Fact] - public void ShouldGenerateOptionsWithEnvFiles() - { - var envFiles = new[] { "test.env", "other.env" }; - var options = new DotEnvOptions() - .WithEnvFiles(envFiles); - - options.IgnoreExceptions - .Should() - .BeTrue(); - options.Encoding - .Should() - .Be(Encoding.UTF8); - options.TrimValues - .Should() - .BeFalse(); - options.OverwriteExistingVars - .Should() - .BeTrue(); - options.ProbeForEnv - .Should() - .BeFalse(); - options.ProbeLevelsToSearch - .Should() - .Be(4); - options.EnvFilePaths - .Should() - .BeEquivalentTo(envFiles); - } - - [Fact] - public void ShouldGenerateOptionsRead() - { - var envFiles = new[] { "quotations.env" }; - var values = new DotEnvOptions() - .WithEnvFiles(envFiles) - .Read(); - - values.Count - .Should() - .BeGreaterThan(0); - } - - [Fact] - public void ShouldGenerateOptionsLoad() - { - var envFiles = new[] { "quotations.env" }; - var action = new Action(() => new DotEnvOptions() - .WithEnvFiles(envFiles) - .Load()); - - action.Should() - .NotThrow(); - } -} \ No newline at end of file diff --git a/tests/dotenv.net.Tests/TestFixtures/VariousValueTypesFixture.cs b/tests/dotenv.net.Tests/TestFixtures/VariousValueTypesFixture.cs deleted file mode 100644 index 0cd4c8d..0000000 --- a/tests/dotenv.net.Tests/TestFixtures/VariousValueTypesFixture.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace dotenv.net.Tests.TestFixtures; - -public class VariousValueTypesFixture : IDisposable -{ - public VariousValueTypesFixture() - { - DotEnv.Fluent() - .WithEnvFiles("value-types.env") - .Load(); - } - - public void Dispose() - { - // do nothing - } -} \ No newline at end of file diff --git a/tests/dotenv.net.Tests/Utilities/EnvReader.Tests.cs b/tests/dotenv.net.Tests/Utilities/EnvReader.Tests.cs deleted file mode 100644 index b405955..0000000 --- a/tests/dotenv.net.Tests/Utilities/EnvReader.Tests.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using dotenv.net.Tests.TestFixtures; -using dotenv.net.Utilities; -using FluentAssertions; -using Xunit; - -namespace dotenv.net.Tests.Utilities; - -public class EnvReaderTests : IClassFixture -{ - [Fact] - public void ShouldReadStringValues() - { - EnvReader.GetStringValue("STRING") - .Should() - .Be("laravel"); - - EnvReader.TryGetStringValue("STRING", out _) - .Should() - .BeTrue(); - - EnvReader.TryGetStringValue("NON_EXISTENT_KEY", out _) - .Should() - .BeFalse(); - - Action action = () => EnvReader.GetStringValue("NON_EXISTENT_KEY"); - action.Should() - .Throw(); - } - - [Fact] - public void ShouldReadIntValues() - { - EnvReader.GetIntValue("INTEGER") - .Should() - .Be(3306); - - EnvReader.TryGetIntValue("INTEGER", out _) - .Should() - .BeTrue(); - - EnvReader.TryGetIntValue("NON_EXISTENT_KEY", out _) - .Should() - .BeFalse(); - - Action action = () => EnvReader.GetIntValue("NON_EXISTENT_KEY"); - action.Should() - .Throw(); - } - - [Fact] - public void ShouldReadDoubleValues() - { - EnvReader.GetDoubleValue("DOUBLE") - .Should() - .Be(2762821981981.37627828722); - - EnvReader.TryGetDoubleValue("DOUBLE", out _) - .Should() - .BeTrue(); - - EnvReader.TryGetDoubleValue("NON_EXISTENT_KEY", out _) - .Should() - .BeFalse(); - - Action action = () => EnvReader.GetDoubleValue("NON_EXISTENT_KEY"); - action.Should() - .Throw(); - } - - [Fact] - public void ShouldReadDecimalValues() - { - EnvReader.GetDecimalValue("DECIMAL") - .Should() - .Be(34.56m); - - EnvReader.TryGetDecimalValue("DECIMAL", out _) - .Should() - .BeTrue(); - - EnvReader.TryGetDecimalValue("NON_EXISTENT_KEY", out _) - .Should() - .BeFalse(); - - Action action = () => EnvReader.GetDecimalValue("NON_EXISTENT_KEY"); - action.Should() - .Throw(); - } - - [Fact] - public void ShouldReadBooleanValues() - { - EnvReader.GetBooleanValue("BOOLEAN") - .Should() - .BeTrue(); - - EnvReader.TryGetBooleanValue("BOOLEAN", out _) - .Should() - .BeTrue(); - - EnvReader.TryGetBooleanValue("NON_EXISTENT_KEY", out _) - .Should() - .BeFalse(); - - Action action = () => EnvReader.GetBooleanValue("NON_EXISTENT_KEY"); - action.Should() - .Throw(); - } - - [Fact] - public void ShouldTellIfAKeyHasAValue() - { - EnvReader.HasValue("BOOLEAN") - .Should() - .BeTrue(); - - EnvReader.HasValue("NON_EXISTENT_KEY") - .Should() - .BeFalse(); - } -} \ No newline at end of file diff --git a/tests/dotenv.net.Tests/ascii.env b/tests/dotenv.net.Tests/ascii.env deleted file mode 100644 index 29ace98..0000000 --- a/tests/dotenv.net.Tests/ascii.env +++ /dev/null @@ -1 +0,0 @@ -ENCODING=ASCII \ No newline at end of file diff --git a/tests/dotenv.net.Tests/dotenv.net.Tests.csproj b/tests/dotenv.net.Tests/dotenv.net.Tests.csproj deleted file mode 100644 index e75eec9..0000000 --- a/tests/dotenv.net.Tests/dotenv.net.Tests.csproj +++ /dev/null @@ -1,54 +0,0 @@ - - - net8.0 - false - - 3.1.0 - 3.1.0 - latest - enable - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - runtime; build; native; contentfiles; analyzers - all - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - Always - - - Always - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - Always - - - PreserveNewest - - - - - - \ No newline at end of file diff --git a/tests/dotenv.net.Tests/generic.env b/tests/dotenv.net.Tests/generic.env deleted file mode 100644 index 5d68bb5..0000000 --- a/tests/dotenv.net.Tests/generic.env +++ /dev/null @@ -1,2 +0,0 @@ - -Generic=Value \ No newline at end of file diff --git a/tests/dotenv.net.Tests/incomplete.env b/tests/dotenv.net.Tests/incomplete.env deleted file mode 100644 index b662a4c..0000000 --- a/tests/dotenv.net.Tests/incomplete.env +++ /dev/null @@ -1,2 +0,0 @@ -=ValueWithNoKey -KeyWithNoValue= \ No newline at end of file diff --git a/tests/dotenv.net.Tests/multi-lines.env b/tests/dotenv.net.Tests/multi-lines.env deleted file mode 100644 index 6e8df71..0000000 --- a/tests/dotenv.net.Tests/multi-lines.env +++ /dev/null @@ -1,24 +0,0 @@ -DOUBLE_QUOTE="double" -DOUBLE_QUOTE_MULTI_LINE="dou -bler" -DOUBLE_QUOTE_EVEN_MORE_LINES="dou - -b - -lest" -DOUBLE_QUOTE_WHITESPACE_LINES = "dou - -b - -lest" -DOUBLE_QUOTE.WITH_DOTS = "dou - -b - -le -dots" -DOUBLE_QUOTE_NO_CLOSE = "dou - -b - -lest the more diff --git a/tests/dotenv.net.Tests/quotations.env b/tests/dotenv.net.Tests/quotations.env deleted file mode 100644 index f7c57db..0000000 --- a/tests/dotenv.net.Tests/quotations.env +++ /dev/null @@ -1,2 +0,0 @@ -DOUBLE_QUOTES="double" -SINGLE_QUOTES='single' diff --git a/tests/dotenv.net.Tests/value-types.env b/tests/dotenv.net.Tests/value-types.env deleted file mode 100644 index a392be4..0000000 --- a/tests/dotenv.net.Tests/value-types.env +++ /dev/null @@ -1,6 +0,0 @@ -STRING=laravel -INTEGER=3306 -BOOLEAN=true -DECIMAL=34.56 -DOUBLE=2762821981981.37627828722 -DOUBLE=2762821981981.37627828722 diff --git a/tests/dotenv.net.Tests/whitespaces.env b/tests/dotenv.net.Tests/whitespaces.env deleted file mode 100644 index e0edd48..0000000 --- a/tests/dotenv.net.Tests/whitespaces.env +++ /dev/null @@ -1,2 +0,0 @@ - DB_DATABASE= laravel - \ No newline at end of file