Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 47 additions & 14 deletions src/dotenv.net.Tests/DotEnvOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,36 @@ public void Constructor_WithNullEncoding_ShouldUseUtf8()
}

[Fact]
public void WithEncoding_WithNullEncoding_ShouldUseUtf8()
public void Constructor_WithProbeForEnvAndNoLevels_ShouldSetDefaults()
{
var options = new DotEnvOptions(probeForEnv: true, envFilePaths: null);
options.ProbeForEnv.ShouldBeTrue();
options.ProbeLevelsToSearch.ShouldBe(DotEnvOptions.DefaultProbeAscendLimit);
}

[Fact]
public void Constructor_WithProbeForEnvAndExplicitLevels_ShouldRespectProvidedLevel()
{
var options = new DotEnvOptions(probeForEnv: true, probeLevelsToSearch: 2, envFilePaths: null);
options.ProbeLevelsToSearch.ShouldBe(2);
}

[Fact]
public void WithEncoding_WithNullEncoding_ShouldThrowException()
{
var options = new DotEnvOptions();
options.WithEncoding(null!);
options.Encoding.ShouldBe(Encoding.UTF8);
Action action = () => options.WithEncoding(null!);
action.ShouldThrow<ArgumentNullException>()
.Message.ShouldBe("Encoding cannot be null (Parameter 'encoding')");
}

[Fact]
public void WithEnvFiles_WithNullParams_ShouldUseDefaultPath()
public void WithEnvFiles_WithNullParams_ShouldThrowException()
{
var options = new DotEnvOptions();
options.WithEnvFiles(null!);
options.EnvFilePaths.ShouldBe([DotEnvOptions.DefaultEnvFileName]);
Action action = () => options.WithEnvFiles(null!);
action.ShouldThrow<ArgumentNullException>()
.Message.ShouldBe("EnvFilePaths cannot be null (Parameter 'envFilePaths')");
}

[Fact]
Expand All @@ -54,6 +71,22 @@ public void WithEnvFiles_WithEmptyParams_ShouldUseDefaultPath()
options.EnvFilePaths.ShouldBe([DotEnvOptions.DefaultEnvFileName]);
}

[Fact]
public void WithEnvFiles_WhenProbeForEnvIsTrue_ShouldThrow()
{
var options = new DotEnvOptions(probeForEnv: true);
var ex = Should.Throw<InvalidOperationException>(() => options.WithEnvFiles("custom.env"));
ex.Message.ShouldBe("EnvFiles paths cannot be set when ProbeForEnv is true");
}

[Fact]
public void WithEnvFiles_WithNonEmptyList_ShouldSetPaths()
{
var options = new DotEnvOptions();
options.WithEnvFiles("test.env");
options.EnvFilePaths.ShouldBe(["test.env"]);
}

[Fact]
public void WithProbeForEnv_WithNegativeProbeLevels_ShouldUseDefaultProbeDepth()
{
Expand All @@ -62,6 +95,14 @@ public void WithProbeForEnv_WithNegativeProbeLevels_ShouldUseDefaultProbeDepth()
options.ProbeLevelsToSearch.ShouldBe(DotEnvOptions.DefaultProbeAscendLimit);
}

[Fact]
public void WithProbeForEnv_WhenCustomEnvFilePathSet_ShouldThrow()
{
var options = new DotEnvOptions(envFilePaths: new[] { "custom.env" });
var ex = Should.Throw<InvalidOperationException>(() => options.WithProbeForEnv());
ex.Message.ShouldBe("Cannot use ProbeForEnv when EnvFiles is set.");
}

[Fact]
public void WithoutProbeForEnv_ShouldResetProbeLevelsToDefault()
{
Expand All @@ -71,14 +112,6 @@ public void WithoutProbeForEnv_ShouldResetProbeLevelsToDefault()
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)]
Expand Down
121 changes: 78 additions & 43 deletions src/dotenv.net.Tests/ReaderTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Shouldly;
using Xunit;
Expand All @@ -10,39 +11,48 @@
public class ReaderTests : IDisposable
{
private readonly string _tempFilePath;
private readonly string _tempDirPath;

private readonly string _testRootPath;
private readonly string _startPath;

public ReaderTests()
{
_tempFilePath = Path.GetTempFileName();
_tempDirPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(_tempDirPath);

// Create a unique root directory for this test run in the system's temp folder.
_testRootPath = Path.Combine(Path.GetTempPath(), "DotEnvTests_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_testRootPath);

_startPath = AppContext.BaseDirectory;
}

public void Dispose()
{
if (File.Exists(_tempFilePath))
File.Delete(_tempFilePath);
if (Directory.Exists(_tempDirPath))
Directory.Delete(_tempDirPath, true);

if (Directory.Exists(_testRootPath))
Directory.Delete(_testRootPath, true);
}

[Theory]
[InlineData(null, false)]

Check warning on line 39 in src/dotenv.net.Tests/ReaderTests.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Null should not be used for type parameter 'path' of type 'string'. Use a non-null value, or convert the parameter to a nullable type. (https://xunit.net/xunit.analyzers/rules/xUnit1012)
[InlineData("", false)]
[InlineData(" ", false)]
public void ReadFileLines_InvalidPathAndIgnoreExceptionsFalse_ShouldThrowArgumentException(string path, bool ignoreExceptions)
public void ReadFileLines_InvalidPathAndIgnoreExceptionsFalse_ShouldThrowArgumentException(string path,
bool ignoreExceptions)
{
Action act = () => Reader.ReadFileLines(path, ignoreExceptions, null);
act.ShouldThrow<ArgumentException>().Message.ShouldContain("The file path cannot be null, empty or whitespace.");
act.ShouldThrow<ArgumentException>().Message
.ShouldContain("The file path cannot be null, empty or whitespace.");
}

[Theory]
[InlineData(null, true)]

Check warning on line 51 in src/dotenv.net.Tests/ReaderTests.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Null should not be used for type parameter 'path' of type 'string'. Use a non-null value, or convert the parameter to a nullable type. (https://xunit.net/xunit.analyzers/rules/xUnit1012)
[InlineData("", true)]
[InlineData(" ", true)]
public void ReadFileLines_InvalidPathAndIgnoreExceptionsTrue_ShouldReturnEmptySpan(string path, bool ignoreExceptions)
public void ReadFileLines_InvalidPathAndIgnoreExceptionsTrue_ShouldReturnEmptySpan(string path,
bool ignoreExceptions)
{
var result = Reader.ReadFileLines(path, ignoreExceptions, null).ToArray();
result.ShouldBeEmpty();
Expand All @@ -51,9 +61,9 @@
[Fact]
public void ReadFileLines_NonExistentFileAndIgnoreExceptionsFalse_ShouldThrowFileNotFoundException()
{
var path = "nonexistent.env";
const string path = "nonexistent.env";
Action act = () => Reader.ReadFileLines(path, false, null);
act.ShouldThrow<FileNotFoundException>().Message.ShouldContain (path);
act.ShouldThrow<FileNotFoundException>().Message.ShouldContain(path);
}

[Fact]
Expand All @@ -76,7 +86,7 @@
[Fact]
public void ReadFileLines_WithCustomEncoding_ShouldReturnCorrectContent()
{
var content = "KEY=üñîçø∂é";
const string content = "KEY=üñîçø∂é";
File.WriteAllText(_tempFilePath, content, Encoding.UTF32);
var result = Reader.ReadFileLines(_tempFilePath, false, Encoding.UTF32);
result[0].ShouldBe(content);
Expand Down Expand Up @@ -109,8 +119,13 @@
[Fact]
public void MergeEnvKeyValues_SingleArray_ShouldReturnAllItems()
{
var input = new[] {
new[] { new KeyValuePair<string, string>("KEY1", "value1"), new KeyValuePair<string, string>("KEY2", "value2") }
var input = new[]
{
new[]
{
new KeyValuePair<string, string>("KEY1", "value1"),
new KeyValuePair<string, string>("KEY2", "value2")
}
};
var result = Reader.MergeEnvKeyValues(input, false);
result.ShouldBe(new Dictionary<string, string> { { "KEY1", "value1" }, { "KEY2", "value2" } });
Expand All @@ -119,7 +134,8 @@
[Fact]
public void MergeEnvKeyValues_MultipleArraysWithoutOverwrite_ShouldKeepFirstValue()
{
var input = new[] {
var input = new[]
{
new[] { new KeyValuePair<string, string>("KEY", "first") },
new[] { new KeyValuePair<string, string>("KEY", "second") }
};
Expand All @@ -131,7 +147,8 @@
[Fact]
public void MergeEnvKeyValues_MultipleArraysWithOverwrite_ShouldKeepLastValue()
{
var input = new[] {
var input = new[]
{
new[] { new KeyValuePair<string, string>("KEY", "first") },
new[] { new KeyValuePair<string, string>("KEY", "second") }
};
Expand All @@ -143,60 +160,78 @@
[Fact]
public void MergeEnvKeyValues_ComplexMerge_ShouldHandleAllCases()
{
var input = new[] {
new[] {
var input = new[]
{
new[]
{
new KeyValuePair<string, string>("KEY1", "value1"),
new KeyValuePair<string, string>("KEY2", "value2")
},
new[] {
new[]
{
new KeyValuePair<string, string>("KEY2", "updated"),
new KeyValuePair<string, string>("KEY3", "value3")
}
};
var result = Reader.MergeEnvKeyValues(input, true);
result.ShouldBe(new Dictionary<string, string> { { "KEY1", "value1" }, { "KEY2", "updated" }, { "KEY3", "value3" } });
result.ShouldBe(new Dictionary<string, string>
{ { "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<FileNotFoundException>()
.Message.ShouldContain(DotEnvOptions.DefaultEnvFileName);
var levelsToSearch = 2;
var exception = Should.Throw<FileNotFoundException>(() =>
{
Reader.GetProbedEnvPath(levelsToSearch, ignoreExceptions: false);
});

exception.Message.ShouldContain($"Could not find '{DotEnvOptions.DefaultEnvFileName}'");
exception.Message.ShouldContain($"after searching {levelsToSearch} directory level(s) upwards.");
exception.Message.ShouldContain("Searched paths:");
exception.Message.ShouldContain(_startPath);
exception.Message.ShouldContain("net9.0");
exception.Message.ShouldContain("Debug");
}

[Fact]
public void GetProbedEnvPath_FileNotFoundAndIgnoreExceptionsTrue_ShouldReturnNull()
public void GetProbedEnvPath_FileNotFoundAndIgnoreExceptionsTrue_ShouldReturnEmpty()
{
using var dir = new TempWorkingDirectory(_tempDirPath);
var result = Reader.GetProbedEnvPath(levelsToSearch: 2, ignoreExceptions: true);
result.ShouldBeNull();
result.ShouldBeEmpty();
}

[Fact]
public void GetProbedEnvPath_LevelsTooLow_ShouldNotFindFile()
public void GetProbedEnvPath_ShouldFindFile_ThreeLevelsUp()
{
var envPath = Path.Combine(_tempDirPath, ".env");
File.WriteAllText(envPath, "TEST=value");
var startDir = Path.Combine(_tempDirPath, "subdir1", "subdir2", "subdir3");
Directory.CreateDirectory(startDir);
var grandParentDirectory = Directory.GetParent(_startPath)!.Parent!.Parent!.Parent!.FullName;
var expectedPath = Path.Combine(grandParentDirectory, DotEnvOptions.DefaultEnvFileName);

using var dir = new TempWorkingDirectory(startDir);
var result = Reader.GetProbedEnvPath(levelsToSearch: 2, ignoreExceptions: true);
result.ShouldBeNull();
var result = Reader.GetProbedEnvPath(levelsToSearch: 3, ignoreExceptions: true).ToList();

result.ShouldHaveSingleItem();
result.First().ShouldBe(expectedPath);
}

private class TempWorkingDirectory : IDisposable
[Fact]
public void GetProbedEnvPath_ShouldReturnEmpty_WhenFileExistsButIsOutOfSearchRange()
{
private readonly string _originalDirectory;
// File is at level 3, but we only search up to level 1.
var result = Reader.GetProbedEnvPath(levelsToSearch: 1, ignoreExceptions: true);

public TempWorkingDirectory(string path)
result.ShouldBeEmpty();
}

[Fact]
public void GetProbedEnvPath_ShouldThrow_WhenFileExistsButIsOutOfSearchRange()
{
// File is at level 3, but we only search up to level 1.
var exception = Should.Throw<FileNotFoundException>(() =>
{
_originalDirectory = Directory.GetCurrentDirectory();
Directory.SetCurrentDirectory(path);
}
Reader.GetProbedEnvPath(levelsToSearch: 1, ignoreExceptions: false);
});

public void Dispose() => Directory.SetCurrentDirectory(_originalDirectory);
exception.Message.ShouldContain($"Could not find '{DotEnvOptions.DefaultEnvFileName}'");
}
}
}
2 changes: 1 addition & 1 deletion src/dotenv.net/DotEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static IDictionary<string, string> Read(DotEnvOptions? options = null)
{
options ??= new DotEnvOptions();
var envFilePaths = options.ProbeForEnv
? [Reader.GetProbedEnvPath(options.ProbeLevelsToSearch, options.IgnoreExceptions)]
? Reader.GetProbedEnvPath(options.ProbeLevelsToSearch!.Value, options.IgnoreExceptions)
: options.EnvFilePaths;
var envFileKeyValues = envFilePaths
.Select(envFilePath =>
Expand Down
Loading
Loading