using FsCheck;
using FsCheck.Xunit;
using System.IO.Compression;
using WorkCameraExport.Models;
using Xunit;
namespace WorkCameraExport.Tests
{
///
/// ZIP 目录结构属性测试
/// Feature: work-camera-2.0.1, Property 9: ZIP 目录结构
/// Validates: Requirements 9.2, 9.3
///
public class ZipDirectoryStructurePropertyTests : IDisposable
{
private readonly string _testOutputDir;
public ZipDirectoryStructurePropertyTests()
{
_testOutputDir = Path.Combine(Path.GetTempPath(), $"ZipStructure_Test_{Guid.NewGuid():N}");
Directory.CreateDirectory(_testOutputDir);
}
public void Dispose()
{
try
{
if (Directory.Exists(_testOutputDir))
{
Directory.Delete(_testOutputDir, true);
}
}
catch
{
// 忽略清理错误
}
}
///
/// Property 9: ZIP 目录结构
/// For any 下载的照片 ZIP 文件,解压后的目录结构应包含:
/// 当日照片/、参与人员/{人员姓名}/、工作内容/{工作内容}/、部门/{部门名称}/ 四个分类目录。
/// Validates: Requirements 9.2, 9.3
///
[Property(MaxTest = 100)]
public Property ZipStructure_ShouldContainRequiredDirectories()
{
return Prop.ForAll(
Arb.From(),
Arb.From(),
(workerCountGen, seedGen) =>
{
var workerCount = (workerCountGen.Get % 3) + 1; // 1-3 个工人
var seed = seedGen.Get;
var deptName = $"部门{seed % 100}";
var content = $"内容{seed % 100}";
var workers = Enumerable.Range(1, workerCount)
.Select(i => $"工人{seed}_{i}")
.ToList();
var yearMonth = "2025-01";
var recordDate = new DateTime(2025, 1, 15);
// 创建测试目录结构
var directories = CreateDirectoryStructure(
_testOutputDir, yearMonth, recordDate, deptName, content, workers);
// 验证目录结构
var basePath = Path.Combine(_testOutputDir, "workfiles", "202501", "20250115");
// 1. 验证当日照片目录存在
var dailyDirExists = Directory.Exists(Path.Combine(basePath, "当日照片"));
// 2. 验证参与人员目录存在
var workerDirsExist = workers.All(w =>
Directory.Exists(Path.Combine(basePath, "参与人员", w)));
// 3. 验证工作内容目录存在
var contentDirExists = Directory.Exists(Path.Combine(basePath, "工作内容", content));
// 4. 验证部门目录存在
var deptDirExists = Directory.Exists(Path.Combine(basePath, "部门", deptName));
// 清理
CleanupDirectories(_testOutputDir);
return (dailyDirExists && workerDirsExist && contentDirExists && deptDirExists)
.Label($"All required directories should exist: daily={dailyDirExists}, workers={workerDirsExist}, content={contentDirExists}, dept={deptDirExists}");
});
}
///
/// Property 9 扩展: 目录结构应按日期组织
///
[Property(MaxTest = 100)]
public Property ZipStructure_ShouldBeOrganizedByDate()
{
return Prop.ForAll(
Arb.From(),
Arb.From(),
(monthGen, dayGen) =>
{
var month = (monthGen.Get % 12) + 1; // 1-12
var day = (dayGen.Get % 28) + 1; // 1-28
var yearMonth = $"2025-{month:D2}";
var recordDate = new DateTime(2025, month, day);
var expectedYYYYMM = $"2025{month:D2}";
var expectedYYYYMMDD = $"2025{month:D2}{day:D2}";
var directories = CreateDirectoryStructure(
_testOutputDir, yearMonth, recordDate, "测试部门", "测试内容", new List { "测试工人" });
// 验证目录路径包含正确的日期格式
var basePath = Path.Combine(_testOutputDir, "workfiles", expectedYYYYMM, expectedYYYYMMDD);
var pathExists = Directory.Exists(basePath);
CleanupDirectories(_testOutputDir);
return pathExists.Label($"Directory path should contain {expectedYYYYMM}/{expectedYYYYMMDD}");
});
}
///
/// Property 9 扩展: 每个工人应有独立的目录
///
[Property(MaxTest = 100)]
public Property ZipStructure_EachWorker_ShouldHaveOwnDirectory()
{
return Prop.ForAll(
Arb.From(),
(workerCountGen) =>
{
var workerCount = (workerCountGen.Get % 5) + 1; // 1-5 个工人
var workers = Enumerable.Range(1, workerCount)
.Select(i => $"工人{i}")
.ToList();
var yearMonth = "2025-01";
var recordDate = new DateTime(2025, 1, 15);
var directories = CreateDirectoryStructure(
_testOutputDir, yearMonth, recordDate, "测试部门", "测试内容", workers);
var basePath = Path.Combine(_testOutputDir, "workfiles", "202501", "20250115", "参与人员");
// 验证每个工人都有独立目录
var allWorkersHaveDir = workers.All(w =>
Directory.Exists(Path.Combine(basePath, w)));
// 验证目录数量正确
var dirCount = Directory.Exists(basePath)
? Directory.GetDirectories(basePath).Length
: 0;
CleanupDirectories(_testOutputDir);
return (allWorkersHaveDir && dirCount == workerCount)
.Label($"Each of {workerCount} workers should have own directory, found {dirCount}");
});
}
///
/// Property 9 扩展: 空工人列表应只创建当日照片目录
///
[Fact]
public void ZipStructure_EmptyWorkers_ShouldStillCreateDailyDirectory()
{
var yearMonth = "2025-01";
var recordDate = new DateTime(2025, 1, 15);
var directories = CreateDirectoryStructure(
_testOutputDir, yearMonth, recordDate, "测试部门", "测试内容", new List());
var basePath = Path.Combine(_testOutputDir, "workfiles", "202501", "20250115");
// 当日照片目录应该存在
Assert.True(Directory.Exists(Path.Combine(basePath, "当日照片")));
// 参与人员目录下应该没有子目录
var workerBasePath = Path.Combine(basePath, "参与人员");
if (Directory.Exists(workerBasePath))
{
Assert.Empty(Directory.GetDirectories(workerBasePath));
}
CleanupDirectories(_testOutputDir);
}
#region 辅助方法
///
/// 创建目录结构(模拟 ExportService.CreateDirectoryStructure)
///
private List CreateDirectoryStructure(
string baseDir,
string yearMonth,
DateTime recordDate,
string deptName,
string content,
List workers)
{
var directories = new List();
var yyyyMM = yearMonth.Replace("-", "");
var yyyyMMdd = recordDate.ToString("yyyyMMdd");
var basePath = Path.Combine(baseDir, "workfiles", yyyyMM, yyyyMMdd);
// 1. 当日照片目录
var dailyDir = Path.Combine(basePath, "当日照片");
EnsureDirectoryExists(dailyDir);
directories.Add(dailyDir);
// 2. 参与人员目录
foreach (var worker in workers.Where(w => !string.IsNullOrEmpty(w)))
{
var workerDir = Path.Combine(basePath, "参与人员", SanitizeFileName(worker));
EnsureDirectoryExists(workerDir);
directories.Add(workerDir);
}
// 3. 工作内容目录
if (!string.IsNullOrEmpty(content))
{
var contentDir = Path.Combine(basePath, "工作内容", SanitizeFileName(content));
EnsureDirectoryExists(contentDir);
directories.Add(contentDir);
}
// 4. 部门目录
if (!string.IsNullOrEmpty(deptName))
{
var deptDir = Path.Combine(basePath, "部门", SanitizeFileName(deptName));
EnsureDirectoryExists(deptDir);
directories.Add(deptDir);
}
return directories;
}
///
/// 确保目录存在
///
private void EnsureDirectoryExists(string path)
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
}
///
/// 清理文件名中的非法字符
///
private string SanitizeFileName(string fileName)
{
var invalidChars = Path.GetInvalidFileNameChars();
var sanitized = fileName;
foreach (var c in invalidChars)
{
sanitized = sanitized.Replace(c, '_');
}
if (sanitized.Length > 50)
{
sanitized = sanitized.Substring(0, 50);
}
return sanitized;
}
///
/// 清理测试目录
///
private void CleanupDirectories(string baseDir)
{
try
{
var workfilesDir = Path.Combine(baseDir, "workfiles");
if (Directory.Exists(workfilesDir))
{
Directory.Delete(workfilesDir, true);
}
}
catch
{
// 忽略清理错误
}
}
#endregion
}
}