315 lines
12 KiB
C#
315 lines
12 KiB
C#
using FsCheck;
|
||
using FsCheck.Xunit;
|
||
using Xunit;
|
||
using ZR.Model.Business.Dto;
|
||
|
||
namespace ZR.Tests
|
||
{
|
||
/// <summary>
|
||
/// 迁移URL更新完整性属性测试
|
||
/// **Feature: work-camera-2.0, Property 7: Migration URL Update Integrity**
|
||
/// **Validates: Requirements 10.3, 10.4**
|
||
/// </summary>
|
||
public class MigrationUrlUpdatePropertyTests
|
||
{
|
||
/// <summary>
|
||
/// COS域名URL
|
||
/// </summary>
|
||
private const string CosDomainUrl = "https://miaoyu-1308826010.cos.ap-shanghai.myqcloud.com";
|
||
|
||
/// <summary>
|
||
/// COS路径前缀
|
||
/// </summary>
|
||
private const string CosPrefixes = "workfiles";
|
||
|
||
/// <summary>
|
||
/// 旧服务器URL前缀
|
||
/// </summary>
|
||
private const string OldServerUrl = "http://old-server.com";
|
||
|
||
/// <summary>
|
||
/// 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**
|
||
/// </summary>
|
||
[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");
|
||
});
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 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**
|
||
/// </summary>
|
||
[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");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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**
|
||
/// </summary>
|
||
[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<MigrationUrlPair>()
|
||
};
|
||
|
||
// 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");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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**
|
||
/// </summary>
|
||
[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<MigrationUrlPair>()
|
||
};
|
||
|
||
// 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}");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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**
|
||
/// </summary>
|
||
[Fact]
|
||
public void EmptyImageUrls_ShouldBeRejected()
|
||
{
|
||
var dto1 = new MigrationUpdateDto
|
||
{
|
||
RecordId = 1,
|
||
ImageUrls = null
|
||
};
|
||
|
||
var dto2 = new MigrationUpdateDto
|
||
{
|
||
RecordId = 1,
|
||
ImageUrls = new List<MigrationUrlPair>()
|
||
};
|
||
|
||
// 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");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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**
|
||
/// </summary>
|
||
[Theory]
|
||
[InlineData(0)]
|
||
[InlineData(-1)]
|
||
[InlineData(-100)]
|
||
public void InvalidRecordId_ShouldBeRejected(int recordId)
|
||
{
|
||
var dto = new MigrationUpdateDto
|
||
{
|
||
RecordId = recordId,
|
||
ImageUrls = new List<MigrationUrlPair>
|
||
{
|
||
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");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证是否为有效的COS URL(与CosService.IsValidCosUrl逻辑一致)
|
||
/// </summary>
|
||
private bool IsValidCosUrl(string url)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(url))
|
||
return false;
|
||
|
||
return url.StartsWith($"{CosDomainUrl}/{CosPrefixes}/");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证更新请求是否有效
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
}
|