using FsCheck;
using FsCheck.Xunit;
using Xunit;
using ZR.Model.Business.Dto;
namespace ZR.Tests
{
///
/// 迁移URL更新完整性属性测试
/// **Feature: work-camera-2.0, Property 7: Migration URL Update Integrity**
/// **Validates: Requirements 10.3, 10.4**
///
public class MigrationUrlUpdatePropertyTests
{
///
/// COS域名URL
///
private const string CosDomainUrl = "https://miaoyu-1308826010.cos.ap-shanghai.myqcloud.com";
///
/// COS路径前缀
///
private const string CosPrefixes = "workfiles";
///
/// 旧服务器URL前缀
///
private const string OldServerUrl = "http://old-server.com";
///
/// Property 7: Migration URL Update Integrity - Valid COS URL Validation
///
/// *For any* migration update request,
/// the system SHALL validate all new URLs are valid COS URLs.
///
/// **Validates: Requirements 10.3, 10.4**
///
[Property(MaxTest = 100)]
public Property ValidCosUrl_ShouldBeAccepted()
{
// Generate valid COS URL components
var urlGen = from year in Gen.Choose(2020, 2025)
from month in Gen.Choose(1, 12)
from day in Gen.Choose(1, 28)
from timestamp in Gen.Choose(100000000, 999999999)
from random in Gen.Choose(1000, 9999)
select new { year, month, day, timestamp, random };
return Prop.ForAll(
urlGen.ToArbitrary(),
u =>
{
var dateFolder = $"{u.year}{u.month:D2}";
var dayFolder = $"{u.year}{u.month:D2}{u.day:D2}";
var fileName = $"{u.timestamp}_{u.random}.jpg";
var validCosUrl = $"{CosDomainUrl}/{CosPrefixes}/{dateFolder}/{dayFolder}/当日照片/{fileName}";
var isValid = IsValidCosUrl(validCosUrl);
if (!isValid)
return false.Label($"Valid COS URL '{validCosUrl}' should be accepted");
return true.Label("Valid COS URL accepted");
});
}
///
/// Property 7: Migration URL Update Integrity - Invalid URL Rejection
///
/// *For any* URL that doesn't match the COS domain pattern,
/// the system SHALL reject it.
///
/// **Validates: Requirements 10.3, 10.4**
///
[Property(MaxTest = 100)]
public Property InvalidUrl_ShouldBeRejected()
{
// Generate various invalid URLs
var invalidUrlGen = Gen.OneOf(
// Old server URLs
Gen.Constant($"{OldServerUrl}/images/test.jpg"),
// Wrong domain
Gen.Constant("https://other-bucket.cos.ap-shanghai.myqcloud.com/workfiles/202501/20250101/test.jpg"),
// Missing prefix
Gen.Constant($"{CosDomainUrl}/other/202501/20250101/test.jpg"),
// HTTP instead of HTTPS
Gen.Constant("http://miaoyu-1308826010.cos.ap-shanghai.myqcloud.com/workfiles/202501/20250101/test.jpg"),
// Empty URL
Gen.Constant(""),
// Null-like
Gen.Constant(" ")
);
return Prop.ForAll(
invalidUrlGen.ToArbitrary(),
invalidUrl =>
{
var isValid = IsValidCosUrl(invalidUrl);
if (isValid)
return false.Label($"Invalid URL '{invalidUrl}' should be rejected");
return true.Label("Invalid URL rejected");
});
}
///
/// Property 7: Migration URL Update Integrity - URL Pair Mapping Preservation
///
/// *For any* migration update request with N URL pairs,
/// the system SHALL preserve the mapping between old and new URLs.
///
/// **Validates: Requirements 10.3, 10.4**
///
[Property(MaxTest = 100)]
public Property UrlPairMapping_ShouldBePreserved()
{
// Generate URL pair count
var pairCountGen = Gen.Choose(1, 5);
return Prop.ForAll(
pairCountGen.ToArbitrary(),
pairCount =>
{
var dto = new MigrationUpdateDto
{
RecordId = 1,
ImageUrls = new List()
};
// Generate URL pairs
for (int i = 0; i < pairCount; i++)
{
var timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds() + i;
var random = new System.Random().Next(1000, 9999);
dto.ImageUrls.Add(new MigrationUrlPair
{
OldUrl = $"{OldServerUrl}/images/image{i}.jpg",
NewUrl = $"{CosDomainUrl}/{CosPrefixes}/202501/20250101/当日照片/{timestamp}_{random}.jpg"
});
}
// Verify: All pairs should have distinct old URLs
var distinctOldUrls = dto.ImageUrls.Select(p => p.OldUrl).Distinct().Count();
if (distinctOldUrls != pairCount)
return false.Label($"Expected {pairCount} distinct old URLs, got {distinctOldUrls}");
// Verify: All pairs should have distinct new URLs
var distinctNewUrls = dto.ImageUrls.Select(p => p.NewUrl).Distinct().Count();
if (distinctNewUrls != pairCount)
return false.Label($"Expected {pairCount} distinct new URLs, got {distinctNewUrls}");
// Verify: Each pair maintains its mapping
for (int i = 0; i < pairCount; i++)
{
var pair = dto.ImageUrls[i];
if (!pair.OldUrl.Contains($"image{i}"))
return false.Label($"Pair {i} old URL mapping broken");
}
return true.Label($"URL pair mapping preserved for {pairCount} pairs");
});
}
///
/// Property 7: Migration URL Update Integrity - All URLs Must Be Valid
///
/// *For any* migration update request,
/// if ANY new URL is invalid, the entire update SHALL fail.
///
/// **Validates: Requirements 10.3, 10.4**
///
[Property(MaxTest = 100)]
public Property AllUrlsMustBeValid_OrUpdateFails()
{
// Generate URL pair count and position of invalid URL
var testGen = from pairCount in Gen.Choose(2, 5)
from invalidPosition in Gen.Choose(0, pairCount - 1)
select new { pairCount, invalidPosition };
return Prop.ForAll(
testGen.ToArbitrary(),
t =>
{
var dto = new MigrationUpdateDto
{
RecordId = 1,
ImageUrls = new List()
};
// Generate URL pairs with one invalid
for (int i = 0; i < t.pairCount; i++)
{
var timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds() + i;
var random = new System.Random().Next(1000, 9999);
var newUrl = i == t.invalidPosition
? $"{OldServerUrl}/images/invalid{i}.jpg" // Invalid URL
: $"{CosDomainUrl}/{CosPrefixes}/202501/20250101/当日照片/{timestamp}_{random}.jpg"; // Valid URL
dto.ImageUrls.Add(new MigrationUrlPair
{
OldUrl = $"{OldServerUrl}/images/image{i}.jpg",
NewUrl = newUrl
});
}
// Validate all URLs
var allValid = dto.ImageUrls.All(p => IsValidCosUrl(p.NewUrl));
if (allValid)
return false.Label($"Should have detected invalid URL at position {t.invalidPosition}");
// Verify: The invalid URL is at the expected position
var invalidUrl = dto.ImageUrls[t.invalidPosition].NewUrl;
if (IsValidCosUrl(invalidUrl))
return false.Label($"URL at position {t.invalidPosition} should be invalid");
return true.Label($"Correctly detected invalid URL at position {t.invalidPosition}");
});
}
///
/// Property 7: Migration URL Update Integrity - Empty Request Handling
///
/// *For any* migration update request with empty or null ImageUrls,
/// the system SHALL reject the request.
///
/// **Validates: Requirements 10.3, 10.4**
///
[Fact]
public void EmptyImageUrls_ShouldBeRejected()
{
var dto1 = new MigrationUpdateDto
{
RecordId = 1,
ImageUrls = null
};
var dto2 = new MigrationUpdateDto
{
RecordId = 1,
ImageUrls = new List()
};
// Verify: Both should be considered invalid
var isValid1 = ValidateUpdateRequest(dto1);
var isValid2 = ValidateUpdateRequest(dto2);
Assert.False(isValid1, "Null ImageUrls should be rejected");
Assert.False(isValid2, "Empty ImageUrls should be rejected");
}
///
/// Property 7: Migration URL Update Integrity - Invalid RecordId Handling
///
/// *For any* migration update request with invalid RecordId,
/// the system SHALL reject the request.
///
/// **Validates: Requirements 10.3, 10.4**
///
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-100)]
public void InvalidRecordId_ShouldBeRejected(int recordId)
{
var dto = new MigrationUpdateDto
{
RecordId = recordId,
ImageUrls = new List
{
new MigrationUrlPair
{
OldUrl = $"{OldServerUrl}/images/test.jpg",
NewUrl = $"{CosDomainUrl}/{CosPrefixes}/202501/20250101/当日照片/test.jpg"
}
}
};
var isValid = ValidateUpdateRequest(dto);
Assert.False(isValid, $"RecordId {recordId} should be rejected");
}
///
/// 验证是否为有效的COS URL(与CosService.IsValidCosUrl逻辑一致)
///
private bool IsValidCosUrl(string url)
{
if (string.IsNullOrWhiteSpace(url))
return false;
return url.StartsWith($"{CosDomainUrl}/{CosPrefixes}/");
}
///
/// 验证更新请求是否有效
///
private bool ValidateUpdateRequest(MigrationUpdateDto dto)
{
if (dto == null)
return false;
if (dto.RecordId <= 0)
return false;
if (dto.ImageUrls == null || dto.ImageUrls.Count == 0)
return false;
return true;
}
}
}