Caddy Manager CI build / docker (push) Failing after 1m2s
Found while investigating an unrelated Gitea compromise: CaddyManager itself was not involved, but reviewing it turned up three things worth closing. Reading a configuration was the only file operation that did not validate the name. Saving, renaming and deleting all reject `..`, `/` and `\`, so the read path was the one way to leave the configuration directory and pull in any `*.caddy` file on the host. The HTTP API happened to be covered, because GET checks the name against the directory listing first, but the UI calls the service directly and nothing stopped it. Forwarded headers were trusted from any peer. That is correct only while the container port is unreachable except through the proxy; the moment it is published, a caller dictates the scheme, host and client address the app believes in. Loopback and private space cover a proxy on a Docker network or on the host, which is the documented deployment, and ignore everyone else. The README never said that the `X-Api-Key` check guards `/api/*` and nothing else, so the UI - which rewrites Caddyfiles and holds the Docker socket - reads as protected when it is not. It now says so, and warns about the specific shape that bit us: a second hostname added for machine callers whose only extra directive is a `tls` line, which serves the unauthenticated UI to anyone who can resolve it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1270 lines
54 KiB
C#
1270 lines
54 KiB
C#
using CaddyManager.Contracts.Configurations.Caddy;
|
|
using CaddyManager.Contracts.Caddy;
|
|
using CaddyManager.Contracts.Configurations;
|
|
using CaddyManager.Contracts.Models.Caddy;
|
|
using CaddyManager.Services.Caddy;
|
|
using CaddyManager.Tests.TestUtilities;
|
|
|
|
namespace CaddyManager.Tests.Services.Caddy;
|
|
|
|
/// <summary>
|
|
/// Tests for CaddyService
|
|
/// </summary>
|
|
public class CaddyServiceTests : IDisposable
|
|
{
|
|
private readonly Mock<IConfigurationsService> _mockConfigurationsService;
|
|
private readonly Mock<ICaddyConfigurationParsingService> _mockParsingService;
|
|
private readonly CaddyService _service;
|
|
private readonly string _tempConfigDir;
|
|
private readonly CaddyServiceConfigurations _testConfigurations;
|
|
|
|
public CaddyServiceTests()
|
|
{
|
|
_mockConfigurationsService = new Mock<IConfigurationsService>();
|
|
_mockParsingService = new Mock<ICaddyConfigurationParsingService>();
|
|
|
|
_tempConfigDir = TestHelper.CreateTempDirectory();
|
|
_testConfigurations = new CaddyServiceConfigurations
|
|
{
|
|
ConfigDir = _tempConfigDir
|
|
};
|
|
|
|
_mockConfigurationsService
|
|
.Setup(x => x.Get<CaddyServiceConfigurations>())
|
|
.Returns(_testConfigurations);
|
|
|
|
_service = new CaddyService(_mockConfigurationsService.Object, _mockParsingService.Object);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
TestHelper.CleanupDirectory(_tempConfigDir);
|
|
}
|
|
|
|
#region GetExistingCaddyConfigurations Tests
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service correctly handles an empty configuration directory by returning an empty list.
|
|
/// Setup: Uses a temporary empty directory as the configuration directory with no Caddy files present.
|
|
/// Expectation: The service should return an empty list without errors, ensuring graceful handling of new or clean Caddy installations where no configurations exist yet.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetExistingCaddyConfigurations_WithEmptyDirectory_ReturnsEmptyList()
|
|
{
|
|
// Act
|
|
var result = _service.GetExistingCaddyConfigurations();
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Should().BeEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service automatically creates the configuration directory if it doesn't exist and returns an empty list.
|
|
/// Setup: Configures the service to use a non-existent directory path for Caddy configuration storage.
|
|
/// Expectation: The service should create the missing directory and return an empty list, ensuring automatic directory initialization for new Caddy deployments.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetExistingCaddyConfigurations_WithNonExistentDirectory_CreatesDirectoryAndReturnsEmptyList()
|
|
{
|
|
// Arrange
|
|
var nonExistentDir = Path.Combine(_tempConfigDir, "nonexistent");
|
|
_testConfigurations.ConfigDir = nonExistentDir;
|
|
|
|
// Act
|
|
var result = _service.GetExistingCaddyConfigurations();
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Should().BeEmpty();
|
|
Directory.Exists(nonExistentDir).Should().BeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service correctly reads and parses existing Caddy configuration files to return populated configuration information.
|
|
/// Setup: Creates test Caddy files in the configuration directory and mocks the parsing service to return expected hostnames, targets, and ports.
|
|
/// Expectation: The service should return configuration info objects with parsed data for each file, enabling comprehensive Caddy configuration management and monitoring.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetExistingCaddyConfigurations_WithCaddyFiles_ReturnsConfigurationInfos()
|
|
{
|
|
// Arrange
|
|
var testContent = TestHelper.SampleCaddyfiles.SimpleReverseProxy;
|
|
File.WriteAllText(Path.Combine(_tempConfigDir, "test1.caddy"), testContent);
|
|
File.WriteAllText(Path.Combine(_tempConfigDir, "test2.caddy"), testContent);
|
|
|
|
_mockParsingService
|
|
.Setup(x => x.GetHostnamesFromCaddyfileContent(testContent))
|
|
.Returns(new List<string> { "example.com" });
|
|
_mockParsingService
|
|
.Setup(x => x.GetReverseProxyTargetFromCaddyfileContent(testContent))
|
|
.Returns("localhost");
|
|
_mockParsingService
|
|
.Setup(x => x.GetReverseProxyPortsFromCaddyfileContent(testContent))
|
|
.Returns(new List<int> { 8080 });
|
|
|
|
// Act
|
|
var result = _service.GetExistingCaddyConfigurations();
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Should().HaveCount(2);
|
|
result.Should().Contain(x => x.FileName == "test1");
|
|
result.Should().Contain(x => x.FileName == "test2");
|
|
result.Should().AllSatisfy(x =>
|
|
{
|
|
x.Hostnames.Should().Contain("example.com");
|
|
x.ReverseProxyHostname.Should().Be("localhost");
|
|
x.ReverseProxyPorts.Should().Contain(8080);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service excludes the global Caddyfile from the list of individual configurations.
|
|
/// Setup: Creates both a global Caddyfile and individual .caddy files in the configuration directory.
|
|
/// Expectation: The service should return only the individual configuration files and exclude the global Caddyfile, maintaining separation between global and site-specific configurations.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetExistingCaddyConfigurations_ExcludesGlobalCaddyfile()
|
|
{
|
|
// Arrange
|
|
var testContent = TestHelper.SampleCaddyfiles.SimpleReverseProxy;
|
|
File.WriteAllText(Path.Combine(_tempConfigDir, "Caddyfile"), testContent);
|
|
File.WriteAllText(Path.Combine(_tempConfigDir, "test.caddy"), testContent);
|
|
|
|
_mockParsingService
|
|
.Setup(x => x.GetHostnamesFromCaddyfileContent(testContent))
|
|
.Returns(new List<string> { "example.com" });
|
|
|
|
// Act
|
|
var result = _service.GetExistingCaddyConfigurations();
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Should().HaveCount(1);
|
|
result.Should().Contain(x => x.FileName == "test");
|
|
result.Should().NotContain(x => x.FileName == "Caddyfile");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service returns configuration files in alphabetical order for consistent presentation.
|
|
/// Setup: Creates multiple Caddy configuration files with names that would naturally sort in a specific order.
|
|
/// Expectation: The service should return configurations sorted alphabetically by filename, ensuring predictable and user-friendly ordering in the Caddy management interface.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetExistingCaddyConfigurations_ReturnsOrderedResults()
|
|
{
|
|
// Arrange
|
|
var testContent = TestHelper.SampleCaddyfiles.SimpleReverseProxy;
|
|
File.WriteAllText(Path.Combine(_tempConfigDir, "zebra.caddy"), testContent);
|
|
File.WriteAllText(Path.Combine(_tempConfigDir, "alpha.caddy"), testContent);
|
|
File.WriteAllText(Path.Combine(_tempConfigDir, "beta.caddy"), testContent);
|
|
|
|
// Act
|
|
var result = _service.GetExistingCaddyConfigurations();
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Should().HaveCount(3);
|
|
result[0].FileName.Should().Be("alpha");
|
|
result[1].FileName.Should().Be("beta");
|
|
result[2].FileName.Should().Be("zebra");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetCaddyConfigurationContent Tests
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service correctly retrieves the content of an existing configuration file.
|
|
/// Setup: Creates a test Caddy configuration file with known content in the configuration directory.
|
|
/// Expectation: The service should return the exact file content, enabling configuration viewing and editing functionality in the Caddy management system.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetCaddyConfigurationContent_WithExistingFile_ReturnsContent()
|
|
{
|
|
// Arrange
|
|
var testContent = TestHelper.SampleCaddyfiles.SimpleReverseProxy;
|
|
var filePath = Path.Combine(_tempConfigDir, "test.caddy");
|
|
File.WriteAllText(filePath, testContent);
|
|
|
|
// Act
|
|
var result = _service.GetCaddyConfigurationContent("test");
|
|
|
|
// Assert
|
|
result.Should().Be(testContent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles requests for non-existent configuration files gracefully.
|
|
/// Setup: Attempts to retrieve content for a configuration file that doesn't exist in the directory.
|
|
/// Expectation: The service should return an empty string rather than throwing exceptions, ensuring robust error handling for missing configuration files.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetCaddyConfigurationContent_WithNonExistentFile_ReturnsEmptyString()
|
|
{
|
|
// Act
|
|
var result = _service.GetCaddyConfigurationContent("nonexistent");
|
|
|
|
// Assert
|
|
result.Should().Be(string.Empty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service refuses to read a configuration whose name walks out of the configuration directory.
|
|
/// Setup: Writes a .caddy file in the parent of the configuration directory and asks for it through a traversing name.
|
|
/// Expectation: The service should return an empty string rather than the file's content, so a caller cannot use the read path to reach files the configuration directory does not own — saving, renaming and deleting already reject the same names.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("../outside")]
|
|
[InlineData("../../outside")]
|
|
[InlineData("subdir/../../outside")]
|
|
public void GetCaddyConfigurationContent_WithTraversingName_ReturnsEmptyString(string configurationName)
|
|
{
|
|
// Arrange
|
|
var parentDir = Directory.GetParent(_tempConfigDir)!.FullName;
|
|
var outsidePath = Path.Combine(parentDir, "outside.caddy");
|
|
File.WriteAllText(outsidePath, "secret { reverse_proxy 10.0.0.1:80 }");
|
|
|
|
try
|
|
{
|
|
// Act
|
|
var result = _service.GetCaddyConfigurationContent(configurationName);
|
|
|
|
// Assert
|
|
result.Should().Be(string.Empty);
|
|
}
|
|
finally
|
|
{
|
|
File.Delete(outsidePath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that an absolute path given as a configuration name cannot escape the configuration directory.
|
|
/// Setup: Passes an absolute path to a .caddy file that exists outside the configuration directory.
|
|
/// Expectation: The service should return an empty string, because Path.Combine would otherwise discard the configuration directory entirely and read the absolute path.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetCaddyConfigurationContent_WithAbsolutePath_ReturnsEmptyString()
|
|
{
|
|
// Arrange
|
|
var parentDir = Directory.GetParent(_tempConfigDir)!.FullName;
|
|
var outsidePath = Path.Combine(parentDir, "absolute-outside.caddy");
|
|
File.WriteAllText(outsidePath, "secret { reverse_proxy 10.0.0.1:80 }");
|
|
|
|
try
|
|
{
|
|
// Act
|
|
var result = _service.GetCaddyConfigurationContent(
|
|
Path.Combine(parentDir, "absolute-outside"));
|
|
|
|
// Assert
|
|
result.Should().Be(string.Empty);
|
|
}
|
|
finally
|
|
{
|
|
File.Delete(outsidePath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service correctly retrieves the content of the global Caddyfile configuration.
|
|
/// Setup: Creates a global Caddyfile with known content in the configuration directory.
|
|
/// Expectation: The service should return the global Caddyfile content, enabling management of global Caddy settings and directives that apply across all sites.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetCaddyConfigurationContent_WithGlobalCaddyfile_ReturnsContent()
|
|
{
|
|
// Arrange
|
|
var testContent = TestHelper.SampleCaddyfiles.SimpleReverseProxy;
|
|
var filePath = Path.Combine(_tempConfigDir, "Caddyfile");
|
|
File.WriteAllText(filePath, testContent);
|
|
|
|
// Act
|
|
var result = _service.GetCaddyConfigurationContent("Caddyfile");
|
|
|
|
// Assert
|
|
result.Should().Be(testContent);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetCaddyGlobalConfigurationContent Tests
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service correctly retrieves global configuration content using the dedicated global configuration method.
|
|
/// Setup: Creates a global Caddyfile with known content in the configuration directory.
|
|
/// Expectation: The service should return the global configuration content, providing specialized access to global Caddy settings and server-wide directives.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetCaddyGlobalConfigurationContent_WithExistingGlobalFile_ReturnsContent()
|
|
{
|
|
// Arrange
|
|
var testContent = TestHelper.SampleCaddyfiles.SimpleReverseProxy;
|
|
var filePath = Path.Combine(_tempConfigDir, "Caddyfile");
|
|
File.WriteAllText(filePath, testContent);
|
|
|
|
// Act
|
|
var result = _service.GetCaddyGlobalConfigurationContent();
|
|
|
|
// Assert
|
|
result.Should().Be(testContent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles missing global configuration files gracefully.
|
|
/// Setup: Attempts to retrieve global configuration content when no global Caddyfile exists.
|
|
/// Expectation: The service should return an empty string, indicating no global configuration is present, which is valid for Caddy installations using only site-specific configurations.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetCaddyGlobalConfigurationContent_WithNonExistentGlobalFile_ReturnsEmptyString()
|
|
{
|
|
// Act
|
|
var result = _service.GetCaddyGlobalConfigurationContent();
|
|
|
|
// Assert
|
|
result.Should().Be(string.Empty);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region SaveCaddyConfiguration Tests
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service successfully saves a valid configuration request to the file system.
|
|
/// Setup: Creates a valid save request with filename and content, then attempts to save it to the configuration directory.
|
|
/// Expectation: The service should save the file successfully and return a success response, enabling configuration persistence and management in the Caddy system.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SaveCaddyConfiguration_WithValidRequest_SavesFileAndReturnsSuccess()
|
|
{
|
|
// Arrange
|
|
var request = new CaddySaveConfigurationRequest
|
|
{
|
|
FileName = "test",
|
|
Content = TestHelper.SampleCaddyfiles.SimpleReverseProxy,
|
|
IsNew = false
|
|
};
|
|
|
|
// Act
|
|
var result = _service.SaveCaddyConfiguration(request);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Success.Should().BeTrue();
|
|
result.Message.Should().Be("Configuration file saved successfully");
|
|
|
|
var filePath = Path.Combine(_tempConfigDir, "test.caddy");
|
|
File.Exists(filePath).Should().BeTrue();
|
|
File.ReadAllText(filePath).Should().Be(request.Content);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service correctly saves global configuration files with the proper Caddyfile name.
|
|
/// Setup: Creates a save request specifically for the global Caddyfile configuration.
|
|
/// Expectation: The service should save the file as "Caddyfile" without extension, maintaining the correct naming convention for global Caddy configuration files.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SaveCaddyConfiguration_WithGlobalCaddyfile_SavesWithCorrectName()
|
|
{
|
|
// Arrange
|
|
var request = new CaddySaveConfigurationRequest
|
|
{
|
|
FileName = "Caddyfile",
|
|
Content = TestHelper.SampleCaddyfiles.SimpleReverseProxy,
|
|
IsNew = false
|
|
};
|
|
|
|
// Act
|
|
var result = _service.SaveCaddyConfiguration(request);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Success.Should().BeTrue();
|
|
|
|
var filePath = Path.Combine(_tempConfigDir, "Caddyfile");
|
|
File.Exists(filePath).Should().BeTrue();
|
|
File.ReadAllText(filePath).Should().Be(request.Content);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service validates filename requirements and rejects empty filenames.
|
|
/// Setup: Creates a save request with an empty filename but valid content.
|
|
/// Expectation: The service should return a failure response with an appropriate error message, preventing invalid file creation and ensuring proper configuration file naming.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SaveCaddyConfiguration_WithEmptyFileName_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var request = new CaddySaveConfigurationRequest
|
|
{
|
|
FileName = "",
|
|
Content = TestHelper.SampleCaddyfiles.SimpleReverseProxy,
|
|
IsNew = false
|
|
};
|
|
|
|
// Act
|
|
var result = _service.SaveCaddyConfiguration(request);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Success.Should().BeFalse();
|
|
result.Message.Should().Be("The configuration file name is required");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service validates filename requirements and rejects whitespace-only filenames.
|
|
/// Setup: Creates a save request with a filename containing only whitespace characters.
|
|
/// Expectation: The service should return a failure response, preventing creation of files with invalid names that could cause file system issues or confusion.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SaveCaddyConfiguration_WithWhitespaceFileName_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var request = new CaddySaveConfigurationRequest
|
|
{
|
|
FileName = " ",
|
|
Content = TestHelper.SampleCaddyfiles.SimpleReverseProxy,
|
|
IsNew = false
|
|
};
|
|
|
|
// Act
|
|
var result = _service.SaveCaddyConfiguration(request);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Success.Should().BeFalse();
|
|
result.Message.Should().Be("The configuration file name is required");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service prevents overwriting existing files when creating new configurations.
|
|
/// Setup: Creates an existing configuration file, then attempts to save a new file with the same name using the IsNew flag.
|
|
/// Expectation: The service should return a failure response, preventing accidental overwriting of existing configurations and protecting against data loss.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SaveCaddyConfiguration_WithNewFileButFileExists_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var filePath = Path.Combine(_tempConfigDir, "existing.caddy");
|
|
File.WriteAllText(filePath, "existing content");
|
|
|
|
var request = new CaddySaveConfigurationRequest
|
|
{
|
|
FileName = "existing",
|
|
Content = TestHelper.SampleCaddyfiles.SimpleReverseProxy,
|
|
IsNew = true
|
|
};
|
|
|
|
// Act
|
|
var result = _service.SaveCaddyConfiguration(request);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Success.Should().BeFalse();
|
|
result.Message.Should().Be("The configuration file already exists");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service successfully creates new configuration files when they don't already exist.
|
|
/// Setup: Creates a save request for a new file with a filename that doesn't exist in the configuration directory.
|
|
/// Expectation: The service should save the new file successfully, enabling creation of new Caddy site configurations and expanding the managed configuration set.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SaveCaddyConfiguration_WithNewFileAndFileDoesNotExist_SavesSuccessfully()
|
|
{
|
|
// Arrange
|
|
var request = new CaddySaveConfigurationRequest
|
|
{
|
|
FileName = "newfile",
|
|
Content = TestHelper.SampleCaddyfiles.SimpleReverseProxy,
|
|
IsNew = true
|
|
};
|
|
|
|
// Act
|
|
var result = _service.SaveCaddyConfiguration(request);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Success.Should().BeTrue();
|
|
result.Message.Should().Be("Configuration file saved successfully");
|
|
|
|
var filePath = Path.Combine(_tempConfigDir, "newfile.caddy");
|
|
File.Exists(filePath).Should().BeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service refuses to save configurations whose file name escapes the configuration directory.
|
|
/// Setup: Provides save requests with traversal segments and directory separators in the file name.
|
|
/// Expectation: The service should reject the request and write nothing outside the configuration directory, so an untrusted caller such as the HTTP API cannot write arbitrary files on the host.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("../escape")]
|
|
[InlineData("../../etc/escape")]
|
|
[InlineData("sub/escape")]
|
|
[InlineData("sub\\escape")]
|
|
public void SaveCaddyConfiguration_WithFileNameEscapingConfigDir_ReturnsFailureAndWritesNothing(string fileName)
|
|
{
|
|
// Arrange
|
|
var request = new CaddySaveConfigurationRequest
|
|
{
|
|
FileName = fileName,
|
|
Content = TestHelper.SampleCaddyfiles.SimpleReverseProxy,
|
|
IsNew = true
|
|
};
|
|
var escapedPath = Path.GetFullPath(Path.Combine(_tempConfigDir, $"{fileName}.caddy"));
|
|
|
|
// Act
|
|
var result = _service.SaveCaddyConfiguration(request);
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.Message.Should().Be("The configuration file name contains invalid characters");
|
|
File.Exists(escapedPath).Should().BeFalse();
|
|
Directory.GetFiles(_tempConfigDir).Should().BeEmpty();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region SaveCaddyGlobalConfiguration Tests
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service successfully saves global configuration content using the dedicated global save method.
|
|
/// Setup: Provides valid global configuration content to be saved as the global Caddyfile.
|
|
/// Expectation: The service should save the global configuration successfully, enabling management of server-wide Caddy settings and global directives.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SaveCaddyGlobalConfiguration_WithValidContent_SavesSuccessfully()
|
|
{
|
|
// Arrange
|
|
var content = TestHelper.SampleCaddyfiles.SimpleReverseProxy;
|
|
|
|
// Act
|
|
var result = _service.SaveCaddyGlobalConfiguration(content);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Success.Should().BeTrue();
|
|
result.Message.Should().Be("Configuration file saved successfully");
|
|
|
|
var filePath = Path.Combine(_tempConfigDir, "Caddyfile");
|
|
File.Exists(filePath).Should().BeTrue();
|
|
File.ReadAllText(filePath).Should().Be(content);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region RenameCaddyConfiguration Tests
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service renames an existing configuration file while preserving its content.
|
|
/// Setup: Creates a configuration file with known content, then requests a rename to an unused name.
|
|
/// Expectation: The service should move the file to the new name, remove the old one and keep the content intact, so a user can correct a configuration's name without losing its reverse proxy definition.
|
|
/// </summary>
|
|
[Fact]
|
|
public void RenameCaddyConfiguration_WithExistingFile_RenamesSuccessfully()
|
|
{
|
|
// Arrange
|
|
const string content = "example.com {\n reverse_proxy localhost:8080\n}";
|
|
File.WriteAllText(Path.Combine(_tempConfigDir, "old-name.caddy"), content);
|
|
|
|
// Act
|
|
var result = _service.RenameCaddyConfiguration("old-name", "new-name");
|
|
|
|
// Assert
|
|
result.Success.Should().BeTrue();
|
|
result.Message.Should().Be("Configuration file renamed successfully");
|
|
File.Exists(Path.Combine(_tempConfigDir, "old-name.caddy")).Should().BeFalse();
|
|
File.ReadAllText(Path.Combine(_tempConfigDir, "new-name.caddy")).Should().Be(content);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service refuses to rename a configuration onto a name that is already taken.
|
|
/// Setup: Creates two configuration files with distinct content, then attempts to rename the first onto the second.
|
|
/// Expectation: The service should fail without touching either file, preventing a rename from silently overwriting an unrelated reverse proxy configuration.
|
|
/// </summary>
|
|
[Fact]
|
|
public void RenameCaddyConfiguration_WithExistingTarget_ReturnsFailureAndLeavesFilesUntouched()
|
|
{
|
|
// Arrange
|
|
var sourcePath = Path.Combine(_tempConfigDir, "source.caddy");
|
|
var targetPath = Path.Combine(_tempConfigDir, "target.caddy");
|
|
File.WriteAllText(sourcePath, "source content");
|
|
File.WriteAllText(targetPath, "target content");
|
|
|
|
// Act
|
|
var result = _service.RenameCaddyConfiguration("source", "target");
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.Message.Should().Be("The configuration file already exists");
|
|
File.ReadAllText(sourcePath).Should().Be("source content");
|
|
File.ReadAllText(targetPath).Should().Be("target content");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service reports a failure when the configuration to rename is not present.
|
|
/// Setup: Requests a rename for a configuration name that has no file in the configuration directory.
|
|
/// Expectation: The service should return a descriptive failure rather than creating anything, so a stale UI listing cannot produce an empty configuration file.
|
|
/// </summary>
|
|
[Fact]
|
|
public void RenameCaddyConfiguration_WithMissingSource_ReturnsFailure()
|
|
{
|
|
// Act
|
|
var result = _service.RenameCaddyConfiguration("does-not-exist", "new-name");
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.Message.Should().Be("The configuration file to rename does not exist");
|
|
File.Exists(Path.Combine(_tempConfigDir, "new-name.caddy")).Should().BeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service rejects an empty new name for a rename.
|
|
/// Setup: Creates a configuration file, then attempts to rename it to whitespace.
|
|
/// Expectation: The service should fail and leave the original file in place, since an unnamed configuration file cannot be addressed or loaded by Caddy.
|
|
/// </summary>
|
|
[Fact]
|
|
public void RenameCaddyConfiguration_WithEmptyNewName_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var sourcePath = Path.Combine(_tempConfigDir, "source.caddy");
|
|
File.WriteAllText(sourcePath, "content");
|
|
|
|
// Act
|
|
var result = _service.RenameCaddyConfiguration("source", " ");
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.Message.Should().Be("The configuration file name is required");
|
|
File.Exists(sourcePath).Should().BeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service protects the global Caddyfile from being renamed in either direction.
|
|
/// Setup: Creates the global Caddyfile plus a regular configuration, then attempts a rename using "Caddyfile" as the source and as the target.
|
|
/// Expectation: Both attempts should fail and leave the files untouched, because Caddy loads the global configuration by that exact name and renaming it would break the whole proxy.
|
|
/// </summary>
|
|
[Fact]
|
|
public void RenameCaddyConfiguration_WithGlobalCaddyfile_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var globalPath = Path.Combine(_tempConfigDir, "Caddyfile");
|
|
var regularPath = Path.Combine(_tempConfigDir, "regular.caddy");
|
|
File.WriteAllText(globalPath, "global content");
|
|
File.WriteAllText(regularPath, "regular content");
|
|
|
|
// Act
|
|
var renameGlobalAway = _service.RenameCaddyConfiguration("Caddyfile", "something-else");
|
|
var renameOntoGlobal = _service.RenameCaddyConfiguration("regular", "Caddyfile");
|
|
|
|
// Assert
|
|
renameGlobalAway.Success.Should().BeFalse();
|
|
renameGlobalAway.Message.Should().Be("The global Caddyfile cannot be renamed");
|
|
renameOntoGlobal.Success.Should().BeFalse();
|
|
renameOntoGlobal.Message.Should().Be("The global Caddyfile cannot be renamed");
|
|
File.ReadAllText(globalPath).Should().Be("global content");
|
|
File.ReadAllText(regularPath).Should().Be("regular content");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service treats a rename to the unchanged name as a successful no-op.
|
|
/// Setup: Creates a configuration file and requests a rename to the exact same name.
|
|
/// Expectation: The service should succeed and leave the file as is, letting the editor call rename unconditionally on save without special-casing an untouched name field.
|
|
/// </summary>
|
|
[Fact]
|
|
public void RenameCaddyConfiguration_WithUnchangedName_SucceedsAsNoOp()
|
|
{
|
|
// Arrange
|
|
var filePath = Path.Combine(_tempConfigDir, "same-name.caddy");
|
|
File.WriteAllText(filePath, "content");
|
|
|
|
// Act
|
|
var result = _service.RenameCaddyConfiguration("same-name", "same-name");
|
|
|
|
// Assert
|
|
result.Success.Should().BeTrue();
|
|
File.ReadAllText(filePath).Should().Be("content");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service allows a rename that only changes letter casing.
|
|
/// Setup: Creates a lowercase configuration file, then renames it to the same name with different casing.
|
|
/// Expectation: The service should succeed and the content should be reachable under the new casing, since on case insensitive file systems a naive collision check would otherwise see the source file as an existing target and reject a legitimate rename.
|
|
/// </summary>
|
|
[Fact]
|
|
public void RenameCaddyConfiguration_WithCaseOnlyChange_RenamesSuccessfully()
|
|
{
|
|
// Arrange
|
|
File.WriteAllText(Path.Combine(_tempConfigDir, "myapp.caddy"), "content");
|
|
|
|
// Act
|
|
var result = _service.RenameCaddyConfiguration("myapp", "MyApp");
|
|
|
|
// Assert
|
|
result.Success.Should().BeTrue();
|
|
File.ReadAllText(Path.Combine(_tempConfigDir, "MyApp.caddy")).Should().Be("content");
|
|
_service.GetExistingCaddyConfigurations().Select(c => c.FileName).Should().BeEquivalentTo(["MyApp"]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service rejects rename targets that would escape the configuration directory or use characters the file system disallows.
|
|
/// Setup: Creates a configuration file, then attempts renames using a path traversal segment, a directory separator and an invalid file name character.
|
|
/// Expectation: Every attempt should fail with an invalid characters message and write nothing outside the configuration directory, since the new name arrives from user input and is used directly to build a file path.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("../escaped")]
|
|
[InlineData("nested/name")]
|
|
[InlineData("invalid\0name")]
|
|
public void RenameCaddyConfiguration_WithUnsafeNewName_ReturnsFailure(string newFileName)
|
|
{
|
|
// Arrange
|
|
var sourcePath = Path.Combine(_tempConfigDir, "source.caddy");
|
|
File.WriteAllText(sourcePath, "content");
|
|
|
|
// Act
|
|
var result = _service.RenameCaddyConfiguration("source", newFileName);
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.Message.Should().Be("The configuration file name contains invalid characters");
|
|
File.ReadAllText(sourcePath).Should().Be("content");
|
|
Directory.GetFiles(_tempConfigDir).Should().HaveCount(1);
|
|
File.Exists(Path.Combine(_tempConfigDir, "..", "escaped.caddy")).Should().BeFalse();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region DeleteCaddyConfigurations Tests
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service successfully deletes multiple existing configuration files.
|
|
/// Setup: Creates multiple test configuration files, then requests deletion of all files by name.
|
|
/// Expectation: The service should delete all specified files and return a success response with the list of deleted configurations, enabling bulk configuration cleanup.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DeleteCaddyConfigurations_WithExistingFiles_DeletesSuccessfully()
|
|
{
|
|
// Arrange
|
|
var file1Path = Path.Combine(_tempConfigDir, "test1.caddy");
|
|
var file2Path = Path.Combine(_tempConfigDir, "test2.caddy");
|
|
File.WriteAllText(file1Path, "content1");
|
|
File.WriteAllText(file2Path, "content2");
|
|
|
|
var configurationNames = new List<string> { "test1", "test2" };
|
|
|
|
// Act
|
|
var result = _service.DeleteCaddyConfigurations(configurationNames);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Success.Should().BeTrue();
|
|
result.Message.Should().Be("Configuration(s) deleted successfully");
|
|
result.DeletedConfigurations.Should().HaveCount(2);
|
|
result.DeletedConfigurations.Should().Contain("test1");
|
|
result.DeletedConfigurations.Should().Contain("test2");
|
|
|
|
File.Exists(file1Path).Should().BeFalse();
|
|
File.Exists(file2Path).Should().BeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles deletion requests for a mix of existing and non-existent files appropriately.
|
|
/// Setup: Creates one existing file and requests deletion of both the existing file and a non-existent file.
|
|
/// Expectation: The service should delete the existing file, report partial failure for the non-existent file, and provide detailed feedback about which operations succeeded or failed.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DeleteCaddyConfigurations_WithNonExistentFiles_ReturnsPartialFailure()
|
|
{
|
|
// Arrange
|
|
var file1Path = Path.Combine(_tempConfigDir, "existing.caddy");
|
|
File.WriteAllText(file1Path, "content");
|
|
|
|
var configurationNames = new List<string> { "existing", "nonexistent" };
|
|
|
|
// Act
|
|
var result = _service.DeleteCaddyConfigurations(configurationNames);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Success.Should().BeFalse();
|
|
result.Message.Should().Be("Failed to delete the following configuration(s): nonexistent");
|
|
result.DeletedConfigurations.Should().HaveCount(1);
|
|
result.DeletedConfigurations.Should().Contain("existing");
|
|
|
|
File.Exists(file1Path).Should().BeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service correctly deletes the global Caddyfile when specifically requested.
|
|
/// Setup: Creates a global Caddyfile and requests its deletion using the proper filename.
|
|
/// Expectation: The service should successfully delete the global configuration file, enabling removal of global Caddy settings when needed for reconfiguration or cleanup.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DeleteCaddyConfigurations_WithGlobalCaddyfile_DeletesCorrectly()
|
|
{
|
|
// Arrange
|
|
var globalFilePath = Path.Combine(_tempConfigDir, "Caddyfile");
|
|
File.WriteAllText(globalFilePath, "global content");
|
|
|
|
var configurationNames = new List<string> { "Caddyfile" };
|
|
|
|
// Act
|
|
var result = _service.DeleteCaddyConfigurations(configurationNames);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Success.Should().BeTrue();
|
|
result.Message.Should().Be("Configuration(s) deleted successfully");
|
|
result.DeletedConfigurations.Should().HaveCount(1);
|
|
result.DeletedConfigurations.Should().Contain("Caddyfile");
|
|
|
|
File.Exists(globalFilePath).Should().BeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles empty deletion requests gracefully without errors.
|
|
/// Setup: Provides an empty list of configuration names for deletion.
|
|
/// Expectation: The service should return a success response with no deleted configurations, handling edge cases where no deletion operations are requested.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DeleteCaddyConfigurations_WithEmptyList_ReturnsSuccess()
|
|
{
|
|
// Arrange
|
|
var configurationNames = new List<string>();
|
|
|
|
// Act
|
|
var result = _service.DeleteCaddyConfigurations(configurationNames);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Success.Should().BeTrue();
|
|
result.Message.Should().Be("Configuration(s) deleted successfully");
|
|
result.DeletedConfigurations.Should().BeEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service refuses to delete configurations whose file name escapes the configuration directory.
|
|
/// Setup: Creates a file outside the configuration directory and asks the service to delete it through a traversal file name.
|
|
/// Expectation: The service should report the name as failed and leave the outside file untouched, so an untrusted caller such as the HTTP API cannot delete arbitrary files on the host.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DeleteCaddyConfigurations_WithFileNameEscapingConfigDir_ReportsFailureAndDeletesNothing()
|
|
{
|
|
// Arrange
|
|
var outsideDir = TestHelper.CreateTempDirectory();
|
|
try
|
|
{
|
|
var outsidePath = Path.Combine(outsideDir, "victim.caddy");
|
|
File.WriteAllText(outsidePath, "content");
|
|
var traversalName = Path.Combine("..", Path.GetFileName(outsideDir), "victim");
|
|
|
|
// Act
|
|
var result = _service.DeleteCaddyConfigurations([traversalName]);
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.Message.Should().Contain(traversalName);
|
|
result.DeletedConfigurations.Should().BeEmpty();
|
|
File.Exists(outsidePath).Should().BeTrue();
|
|
}
|
|
finally
|
|
{
|
|
TestHelper.CleanupDirectory(outsideDir);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetCaddyConfigurationInfo Tests
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service correctly retrieves and parses configuration information for an existing file.
|
|
/// Setup: Creates a test configuration file and mocks the parsing service to return expected hostnames, reverse proxy targets, and ports.
|
|
/// Expectation: The service should return a populated configuration info object with all parsed data, enabling detailed configuration analysis and management features.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetCaddyConfigurationInfo_WithExistingFile_ReturnsPopulatedInfo()
|
|
{
|
|
// Arrange
|
|
var testContent = TestHelper.SampleCaddyfiles.SimpleReverseProxy;
|
|
var filePath = Path.Combine(_tempConfigDir, "test.caddy");
|
|
File.WriteAllText(filePath, testContent);
|
|
|
|
var expectedHostnames = new List<string> { "example.com" };
|
|
var expectedTarget = "localhost";
|
|
var expectedPorts = new List<int> { 8080 };
|
|
var expectedTags = new List<string>();
|
|
|
|
_mockParsingService
|
|
.Setup(x => x.GetHostnamesFromCaddyfileContent(testContent))
|
|
.Returns(expectedHostnames);
|
|
_mockParsingService
|
|
.Setup(x => x.GetReverseProxyTargetFromCaddyfileContent(testContent))
|
|
.Returns(expectedTarget);
|
|
_mockParsingService
|
|
.Setup(x => x.GetReverseProxyPortsFromCaddyfileContent(testContent))
|
|
.Returns(expectedPorts);
|
|
_mockParsingService
|
|
.Setup(x => x.GetTagsFromCaddyfileContent(testContent))
|
|
.Returns(expectedTags);
|
|
|
|
// Act
|
|
var result = _service.GetCaddyConfigurationInfo("test");
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Hostnames.Should().BeEquivalentTo(expectedHostnames);
|
|
result.ReverseProxyHostname.Should().Be(expectedTarget);
|
|
result.ReverseProxyPorts.Should().BeEquivalentTo(expectedPorts);
|
|
result.Tags.Should().BeEquivalentTo(expectedTags);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service correctly populates the Tags property when configuration content contains tags.
|
|
/// Setup: Creates a configuration file with tags comment and mocks the parsing service to return expected tags.
|
|
/// Expectation: The service should correctly populate the Tags property using the parsing service, ensuring tag information is available for configuration management.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetCaddyConfigurationInfo_WithTags_PopulatesTagsCorrectly()
|
|
{
|
|
// Arrange
|
|
var testContent = @"
|
|
# Tags: [web;production;ssl]
|
|
example.com {
|
|
reverse_proxy localhost:8080
|
|
}";
|
|
var filePath = Path.Combine(_tempConfigDir, "test-with-tags.caddy");
|
|
File.WriteAllText(filePath, testContent);
|
|
|
|
var expectedHostnames = new List<string> { "example.com" };
|
|
var expectedTarget = "localhost";
|
|
var expectedPorts = new List<int> { 8080 };
|
|
var expectedTags = new List<string> { "web", "production", "ssl" };
|
|
|
|
_mockParsingService
|
|
.Setup(x => x.GetHostnamesFromCaddyfileContent(testContent))
|
|
.Returns(expectedHostnames);
|
|
_mockParsingService
|
|
.Setup(x => x.GetReverseProxyTargetFromCaddyfileContent(testContent))
|
|
.Returns(expectedTarget);
|
|
_mockParsingService
|
|
.Setup(x => x.GetReverseProxyPortsFromCaddyfileContent(testContent))
|
|
.Returns(expectedPorts);
|
|
_mockParsingService
|
|
.Setup(x => x.GetTagsFromCaddyfileContent(testContent))
|
|
.Returns(expectedTags);
|
|
|
|
// Act
|
|
var result = _service.GetCaddyConfigurationInfo("test-with-tags");
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Hostnames.Should().BeEquivalentTo(expectedHostnames);
|
|
result.ReverseProxyHostname.Should().Be(expectedTarget);
|
|
result.ReverseProxyPorts.Should().BeEquivalentTo(expectedPorts);
|
|
result.Tags.Should().BeEquivalentTo(expectedTags);
|
|
result.Tags.Should().HaveCount(3);
|
|
result.Tags.Should().Contain("web");
|
|
result.Tags.Should().Contain("production");
|
|
result.Tags.Should().Contain("ssl");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles requests for configuration information of non-existent files gracefully.
|
|
/// Setup: Requests configuration information for a file that doesn't exist in the configuration directory.
|
|
/// Expectation: The service should return an empty configuration info object rather than throwing exceptions, ensuring robust error handling for missing configuration files.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetCaddyConfigurationInfo_WithNonExistentFile_ReturnsEmptyInfo()
|
|
{
|
|
// Act
|
|
var result = _service.GetCaddyConfigurationInfo("nonexistent");
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Hostnames.Should().BeEmpty();
|
|
result.ReverseProxyHostname.Should().Be(string.Empty);
|
|
result.ReverseProxyPorts.Should().BeEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles empty configuration files by returning empty configuration information.
|
|
/// Setup: Creates an empty configuration file and requests its configuration information.
|
|
/// Expectation: The service should return an empty configuration info object, properly handling edge cases where configuration files exist but contain no parseable content.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetCaddyConfigurationInfo_WithEmptyFile_ReturnsEmptyInfo()
|
|
{
|
|
// Arrange
|
|
var filePath = Path.Combine(_tempConfigDir, "empty.caddy");
|
|
File.WriteAllText(filePath, string.Empty);
|
|
|
|
// Act
|
|
var result = _service.GetCaddyConfigurationInfo("empty");
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Hostnames.Should().BeEmpty();
|
|
result.ReverseProxyHostname.Should().Be(string.Empty);
|
|
result.ReverseProxyPorts.Should().BeEmpty();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Additional Edge Cases and Error Scenarios
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles file system permission errors gracefully when trying to read configuration files.
|
|
/// Setup: Creates a file with restricted permissions and attempts to read its content.
|
|
/// Expectation: The service should return an empty string for the content rather than throwing an exception, ensuring robust error handling for file system permission issues in production environments.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetCaddyConfigurationContent_WithPermissionError_ReturnsEmptyString()
|
|
{
|
|
// Arrange
|
|
var filePath = Path.Combine(_tempConfigDir, "restricted.caddy");
|
|
File.WriteAllText(filePath, "test content");
|
|
|
|
// Make file read-only to simulate permission issues
|
|
var fileInfo = new FileInfo(filePath);
|
|
fileInfo.Attributes = FileAttributes.ReadOnly;
|
|
|
|
try
|
|
{
|
|
// Act
|
|
var result = _service.GetCaddyConfigurationContent("restricted");
|
|
|
|
// Assert
|
|
// The service should still be able to read the file even if it's read-only
|
|
// This test verifies that the service handles file operations gracefully
|
|
result.Should().Be("test content");
|
|
}
|
|
finally
|
|
{
|
|
// Cleanup
|
|
fileInfo.Attributes = FileAttributes.Normal;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles invalid file paths gracefully when saving configurations.
|
|
/// Setup: Attempts to save a configuration with an invalid file path containing illegal characters.
|
|
/// Expectation: The service should return a failure response with an appropriate error message, preventing file system errors and ensuring robust error handling for invalid file paths.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SaveCaddyConfiguration_WithInvalidFilePath_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var request = new CaddySaveConfigurationRequest
|
|
{
|
|
FileName = "invalid<>file",
|
|
Content = "test content",
|
|
IsNew = true
|
|
};
|
|
|
|
// Act
|
|
var result = _service.SaveCaddyConfiguration(request);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
// The service should handle invalid characters gracefully
|
|
// This test verifies that the service doesn't crash with invalid file names
|
|
result.Success.Should().BeTrue();
|
|
result.Message.Should().NotBeNullOrEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles very large configuration files without performance issues.
|
|
/// Setup: Creates a configuration request with a very large content string to simulate large Caddy configurations.
|
|
/// Expectation: The service should successfully save the large configuration without throwing exceptions or experiencing significant performance degradation, ensuring the system can handle production-sized Caddy configurations.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SaveCaddyConfiguration_WithLargeContent_SavesSuccessfully()
|
|
{
|
|
// Arrange
|
|
var largeContent = new string('a', 1000000); // 1MB content
|
|
var request = new CaddySaveConfigurationRequest
|
|
{
|
|
FileName = "large-config",
|
|
Content = largeContent,
|
|
IsNew = true
|
|
};
|
|
|
|
// Act
|
|
var result = _service.SaveCaddyConfiguration(request);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Success.Should().BeTrue();
|
|
|
|
var filePath = Path.Combine(_tempConfigDir, "large-config.caddy");
|
|
File.Exists(filePath).Should().BeTrue();
|
|
File.ReadAllText(filePath).Should().Be(largeContent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles concurrent file access scenarios gracefully.
|
|
/// Setup: Creates multiple threads attempting to save configurations simultaneously to simulate concurrent access.
|
|
/// Expectation: The service should handle concurrent access without throwing exceptions, ensuring thread safety in multi-user environments where multiple users might be editing Caddy configurations simultaneously.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task SaveCaddyConfiguration_WithConcurrentAccess_HandlesGracefully()
|
|
{
|
|
// Arrange
|
|
var tasks = new List<Task<CaddyOperationResponse>>();
|
|
var request = new CaddySaveConfigurationRequest
|
|
{
|
|
FileName = "concurrent-test",
|
|
Content = "test content",
|
|
IsNew = true
|
|
};
|
|
|
|
// Act - Create multiple concurrent save operations
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
tasks.Add(Task.Run(() => _service.SaveCaddyConfiguration(request)));
|
|
}
|
|
|
|
// Wait for all tasks to complete
|
|
await Task.WhenAll(tasks);
|
|
|
|
// Assert
|
|
tasks.Should().AllSatisfy(task => task.Result.Should().NotBeNull());
|
|
// At least one should succeed, others might fail due to file already existing
|
|
tasks.Should().Contain(task => task.Result.Success);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles network file system scenarios where files might be temporarily unavailable.
|
|
/// Setup: Simulates a scenario where the configuration directory becomes temporarily inaccessible.
|
|
/// Expectation: The service should handle temporary file system unavailability gracefully, ensuring robust operation in network file system environments where connectivity might be intermittent.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetExistingCaddyConfigurations_WithTemporarilyUnavailableDirectory_HandlesGracefully()
|
|
{
|
|
// Arrange
|
|
var tempDir = Path.Combine(_tempConfigDir, "temp-unavailable");
|
|
Directory.CreateDirectory(tempDir);
|
|
_testConfigurations.ConfigDir = tempDir;
|
|
|
|
// Create a file to ensure directory exists
|
|
File.WriteAllText(Path.Combine(tempDir, "test.caddy"), "content");
|
|
|
|
// Act
|
|
var result = _service.GetExistingCaddyConfigurations();
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
// Should handle gracefully even if directory becomes temporarily unavailable
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles configuration names with special characters and Unicode properly.
|
|
/// Setup: Attempts to save and retrieve configurations with names containing special characters and Unicode.
|
|
/// Expectation: The service should handle special characters and Unicode in configuration names correctly, ensuring compatibility with international domain names and special naming conventions.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("config-with-unicode-测试")]
|
|
[InlineData("config-with-special-chars!@#$%")]
|
|
[InlineData("config-with-spaces and-dashes")]
|
|
[InlineData("config.with.dots")]
|
|
public void SaveCaddyConfiguration_WithSpecialCharacters_HandlesCorrectly(string fileName)
|
|
{
|
|
// Arrange
|
|
var request = new CaddySaveConfigurationRequest
|
|
{
|
|
FileName = fileName,
|
|
Content = "test content",
|
|
IsNew = true
|
|
};
|
|
|
|
// Act
|
|
var result = _service.SaveCaddyConfiguration(request);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Success.Should().BeTrue();
|
|
|
|
var filePath = Path.Combine(_tempConfigDir, $"{fileName}.caddy");
|
|
File.Exists(filePath).Should().BeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles null content gracefully when saving configurations.
|
|
/// Setup: Attempts to save a configuration with null content.
|
|
/// Expectation: The service should handle null content gracefully, either by treating it as empty string or providing appropriate error handling, ensuring robust operation with null inputs.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SaveCaddyConfiguration_WithNullContent_HandlesGracefully()
|
|
{
|
|
// Arrange
|
|
var request = new CaddySaveConfigurationRequest
|
|
{
|
|
FileName = "null-content-test",
|
|
Content = null!,
|
|
IsNew = true
|
|
};
|
|
|
|
// Act
|
|
var result = _service.SaveCaddyConfiguration(request);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Success.Should().BeTrue();
|
|
|
|
var filePath = Path.Combine(_tempConfigDir, "null-content-test.caddy");
|
|
File.Exists(filePath).Should().BeTrue();
|
|
File.ReadAllText(filePath).Should().Be(string.Empty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles disk space issues gracefully when saving large configurations.
|
|
/// Setup: Attempts to save a configuration that would exceed available disk space (simulated).
|
|
/// Expectation: The service should return a failure response with an appropriate error message when disk space is insufficient, ensuring proper error reporting for resource constraint scenarios.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SaveCaddyConfiguration_WithInsufficientDiskSpace_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var request = new CaddySaveConfigurationRequest
|
|
{
|
|
FileName = "disk-space-test",
|
|
Content = new string('a', 1000000), // Large content
|
|
IsNew = true
|
|
};
|
|
|
|
// Act
|
|
var result = _service.SaveCaddyConfiguration(request);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
// In a real scenario with insufficient disk space, this would fail
|
|
// For this test, we're just ensuring the method handles large content gracefully
|
|
result.Success.Should().BeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the Caddy service handles file system corruption scenarios gracefully.
|
|
/// Setup: Creates a scenario where the configuration directory structure is corrupted or invalid.
|
|
/// Expectation: The service should handle file system corruption gracefully, either by creating necessary directories or providing appropriate error messages, ensuring robust operation in degraded file system conditions.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetExistingCaddyConfigurations_WithCorruptedDirectory_HandlesGracefully()
|
|
{
|
|
// Arrange
|
|
var corruptedDir = Path.Combine(_tempConfigDir, "corrupted");
|
|
_testConfigurations.ConfigDir = corruptedDir;
|
|
|
|
// Act
|
|
var result = _service.GetExistingCaddyConfigurations();
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Should().BeEmpty();
|
|
Directory.Exists(corruptedDir).Should().BeTrue();
|
|
}
|
|
|
|
#endregion
|
|
} |