Files
Axi_Omron/tests/AxiOmron.PcbCheck.Tests/LocalFileCheckProviderTests.cs
yunxiao.zhu c67509ec82 ✨ feat(file-check): 添加 PLC 应答后命中文件删除
* 新增默认关闭的删除开关,仅在收到 PLC 放行应答后执行

* 本地与 SFTP Provider 校验根目录并支持幂等删除

* 补充工作流、设置页和文件删除相关测试与文档
2026-08-01 16:42:28 +08:00

234 lines
7.7 KiB
C#

using AxiOmron.PcbCheck.Models;
using AxiOmron.PcbCheck.Options;
using AxiOmron.PcbCheck.Services.Implementations;
using AxiOmron.PcbCheck.Services.Interfaces;
using Xunit;
namespace AxiOmron.PcbCheck.Tests;
/// <summary>
/// 验证本地磁盘文件校验提供程序的目录检查与文件匹配行为。
/// </summary>
public sealed class LocalFileCheckProviderTests : IDisposable
{
private readonly string _rootPath;
/// <summary>
/// 初始化临时测试目录。
/// </summary>
public LocalFileCheckProviderTests()
{
_rootPath = Path.Combine(Path.GetTempPath(), "AxiOmronLocalFileCheckTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_rootPath);
}
/// <summary>
/// 清理临时测试目录。
/// </summary>
public void Dispose()
{
if (Directory.Exists(_rootPath))
{
Directory.Delete(_rootPath, recursive: true);
}
}
/// <summary>
/// 本地根目录为空时,应返回配置异常。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task TestConnectionAsync_ShouldReturnConfigurationError_WhenRootPathIsEmpty()
{
var provider = CreateProvider();
var config = new AppConfig
{
FileCheck = new FileCheckOptions
{
Local = new LocalFileCheckOptions { RootPath = string.Empty }
}
};
FileCheckTestOutcome outcome = await provider.TestConnectionAsync(config, CancellationToken.None);
Assert.True(outcome.IsConfigurationError);
Assert.Contains("本地文件校验目录未配置", outcome.StatusMessage);
}
/// <summary>
/// 本地根目录不存在时,应返回配置异常。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task TestConnectionAsync_ShouldReturnConfigurationError_WhenRootPathDoesNotExist()
{
var provider = CreateProvider();
var config = CreateConfig(Path.Combine(_rootPath, "missing"));
FileCheckTestOutcome outcome = await provider.TestConnectionAsync(config, CancellationToken.None);
Assert.True(outcome.IsConfigurationError);
Assert.Contains("本地文件校验目录不存在", outcome.StatusMessage);
}
/// <summary>
/// 文件名精确匹配模板时,应返回命中文件路径。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task CheckFileAsync_ShouldMatchExactFileName()
{
string filePath = Path.Combine(_rootPath, "PCB-001.txt");
await File.WriteAllTextAsync(filePath, "ok");
var provider = CreateProvider();
FileCheckOutcome outcome = await provider.CheckFileAsync("PCB-001", CreateConfig(_rootPath), CancellationToken.None);
Assert.True(outcome.Exists);
Assert.Equal(filePath, outcome.MatchedFilePath);
}
/// <summary>
/// 文件名只包含条码但不等于模板生成文件名时,不应命中。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task CheckFileAsync_ShouldNotMatchFileNameContainingBarcodeOnly()
{
string filePath = Path.Combine(_rootPath, "abcd10.txt");
await File.WriteAllTextAsync(filePath, "ok");
var provider = CreateProvider();
FileCheckOutcome outcome = await provider.CheckFileAsync("abcd1", CreateConfig(_rootPath), CancellationToken.None);
Assert.False(outcome.Exists);
Assert.Empty(outcome.MatchedFilePath);
}
/// <summary>
/// 文件名模板包含通配符时,应按通配符匹配。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task CheckFileAsync_ShouldMatchWildcardPattern()
{
string filePath = Path.Combine(_rootPath, "PCB-003_result.txt");
await File.WriteAllTextAsync(filePath, "ok");
var provider = CreateProvider();
AppConfig config = CreateConfig(_rootPath);
config.FileCheck.FileNamePattern = "${barcode}_*.txt";
FileCheckOutcome outcome = await provider.CheckFileAsync("PCB-003", config, CancellationToken.None);
Assert.True(outcome.Exists);
Assert.Equal(filePath, outcome.MatchedFilePath);
}
/// <summary>
/// 删除目标位于配置根目录内时,应删除文件并返回成功。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task DeleteFileAsync_ShouldDeleteFile_WhenFileIsWithinConfiguredRoot()
{
string filePath = Path.Combine(_rootPath, "PCB-DELETE-001.txt");
await File.WriteAllTextAsync(filePath, "ok");
var provider = CreateProvider();
FileDeleteOutcome outcome = await provider.DeleteFileAsync(
filePath,
CreateConfig(_rootPath),
CancellationToken.None);
Assert.True(outcome.IsSuccess);
Assert.False(outcome.WasAlreadyMissing);
Assert.False(File.Exists(filePath));
}
/// <summary>
/// 删除目标位于配置根目录之外时,应拒绝删除以防止误删其他文件。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task DeleteFileAsync_ShouldRejectFile_WhenFileIsOutsideConfiguredRoot()
{
string parentPath = Directory.GetParent(_rootPath)!.FullName;
string filePath = Path.Combine(parentPath, $"{Guid.NewGuid():N}.txt");
await File.WriteAllTextAsync(filePath, "keep");
var provider = CreateProvider();
try
{
FileDeleteOutcome outcome = await provider.DeleteFileAsync(
filePath,
CreateConfig(_rootPath),
CancellationToken.None);
Assert.False(outcome.IsSuccess);
Assert.Contains("根目录之外", outcome.ErrorMessage);
Assert.True(File.Exists(filePath));
}
finally
{
File.Delete(filePath);
}
}
/// <summary>
/// 创建本地文件校验提供程序。
/// </summary>
/// <returns>本地文件校验提供程序。</returns>
private static LocalFileCheckProvider CreateProvider()
{
return new LocalFileCheckProvider(new FakeAppLogger<LocalFileCheckProvider>());
}
/// <summary>
/// 创建本地文件校验测试配置。
/// </summary>
/// <param name="rootPath">本地根目录。</param>
/// <returns>测试配置。</returns>
private static AppConfig CreateConfig(string rootPath)
{
return new AppConfig
{
FileCheck = new FileCheckOptions
{
Provider = FileCheckProvider.Local,
FileNamePattern = "${barcode}.txt",
Local = new LocalFileCheckOptions
{
RootPath = rootPath
}
}
};
}
/// <summary>
/// 提供本地文件校验测试用的空日志实现。
/// </summary>
/// <typeparam name="TCategoryName">日志分类类型。</typeparam>
private sealed class FakeAppLogger<TCategoryName> : IAppLogger<TCategoryName>
{
public void LogError(string message, bool showInUi = true, params object?[] args)
{
}
public void LogError(Exception exception, string message, bool showInUi = true, params object?[] args)
{
}
public void LogInformation(string message, bool showInUi = true, params object?[] args)
{
}
public void LogWarning(string message, bool showInUi = true, params object?[] args)
{
}
public void LogWarning(Exception exception, string message, bool showInUi = true, params object?[] args)
{
}
}
}