831 lines
28 KiB
C#
831 lines
28 KiB
C#
using FsCheck;
|
|
using FsCheck.Xunit;
|
|
using HoneyBox.Admin.Business.Models;
|
|
using HoneyBox.Admin.Business.Models.Announcement;
|
|
using HoneyBox.Admin.Business.Services;
|
|
using HoneyBox.Model.Data;
|
|
using HoneyBox.Model.Entities;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace HoneyBox.Tests.Services;
|
|
|
|
/// <summary>
|
|
/// AnnouncementService 属性测试
|
|
/// 测试首页中大奖公告功能的核心属性
|
|
/// </summary>
|
|
public class AnnouncementServicePropertyTests
|
|
{
|
|
private readonly Mock<ILogger<AnnouncementService>> _mockLogger = new();
|
|
|
|
private (HoneyBoxDbContext dbContext, AnnouncementService service) CreateService()
|
|
{
|
|
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
|
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
|
.Options;
|
|
var dbContext = new HoneyBoxDbContext(options);
|
|
var service = new AnnouncementService(dbContext, _mockLogger.Object);
|
|
return (dbContext, service);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 生成有效的非空字符串(用于必填字段)
|
|
/// </summary>
|
|
private static string GenerateValidString(string prefix, int seed)
|
|
{
|
|
return $"{prefix}_{Math.Abs(seed) % 1000}";
|
|
}
|
|
|
|
#region Property 1: CRUD Round-Trip Consistency
|
|
|
|
/// <summary>
|
|
/// **Feature: homepage-prize-announcement, Property 1: CRUD Round-Trip Consistency**
|
|
/// For any valid announcement data, creating an announcement and then reading it back
|
|
/// should return the same data.
|
|
/// **Validates: Requirements 1.1, 1.2, 1.3, 2.4**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool CrudRoundTrip_CreateAndRead_ShouldReturnSameData(
|
|
NonEmptyString userName,
|
|
NonEmptyString prizeLevel,
|
|
NonEmptyString prizeName,
|
|
PositiveInt sort,
|
|
bool isEnabled)
|
|
{
|
|
var actualUserName = userName.Get.Trim();
|
|
var actualPrizeLevel = prizeLevel.Get.Trim();
|
|
var actualPrizeName = prizeName.Get.Trim();
|
|
var actualSort = sort.Get % 1000;
|
|
|
|
// Skip if any required field becomes empty after trim
|
|
if (string.IsNullOrWhiteSpace(actualUserName) ||
|
|
string.IsNullOrWhiteSpace(actualPrizeLevel) ||
|
|
string.IsNullOrWhiteSpace(actualPrizeName))
|
|
{
|
|
return true; // Skip this test case
|
|
}
|
|
|
|
var (dbContext, service) = CreateService();
|
|
|
|
try
|
|
{
|
|
// Create announcement
|
|
var createRequest = new CreateAnnouncementRequest
|
|
{
|
|
UserAvatar = "http://test.com/avatar.jpg",
|
|
UserName = actualUserName,
|
|
PrizeLevel = actualPrizeLevel,
|
|
PrizeName = actualPrizeName,
|
|
Sort = actualSort,
|
|
IsEnabled = isEnabled
|
|
};
|
|
|
|
var created = service.CreateAsync(createRequest).GetAwaiter().GetResult();
|
|
|
|
// Read it back
|
|
var retrieved = service.GetByIdAsync(created.Id).GetAwaiter().GetResult();
|
|
|
|
// Verify data consistency
|
|
return retrieved.UserName == actualUserName &&
|
|
retrieved.PrizeLevel == actualPrizeLevel &&
|
|
retrieved.PrizeName == actualPrizeName &&
|
|
retrieved.Sort == actualSort &&
|
|
retrieved.IsEnabled == isEnabled &&
|
|
retrieved.UserAvatar == "http://test.com/avatar.jpg";
|
|
}
|
|
finally
|
|
{
|
|
dbContext.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// **Feature: homepage-prize-announcement, Property 1: CRUD Round-Trip Consistency**
|
|
/// For any valid announcement data, updating it and reading again should reflect the updates.
|
|
/// **Validates: Requirements 1.1, 1.2, 1.3, 2.4**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool CrudRoundTrip_UpdateAndRead_ShouldReflectUpdates(
|
|
PositiveInt seed1,
|
|
PositiveInt seed2,
|
|
PositiveInt sort1,
|
|
PositiveInt sort2,
|
|
bool isEnabled1,
|
|
bool isEnabled2)
|
|
{
|
|
var userName1 = GenerateValidString("User", seed1.Get);
|
|
var prizeLevel1 = GenerateValidString("Level", seed1.Get);
|
|
var prizeName1 = GenerateValidString("Prize", seed1.Get);
|
|
var userName2 = GenerateValidString("UpdatedUser", seed2.Get);
|
|
var prizeLevel2 = GenerateValidString("UpdatedLevel", seed2.Get);
|
|
var prizeName2 = GenerateValidString("UpdatedPrize", seed2.Get);
|
|
var actualSort1 = sort1.Get % 1000;
|
|
var actualSort2 = sort2.Get % 1000;
|
|
|
|
var (dbContext, service) = CreateService();
|
|
|
|
try
|
|
{
|
|
// Create initial announcement
|
|
var createRequest = new CreateAnnouncementRequest
|
|
{
|
|
UserAvatar = "http://test.com/avatar1.jpg",
|
|
UserName = userName1,
|
|
PrizeLevel = prizeLevel1,
|
|
PrizeName = prizeName1,
|
|
Sort = actualSort1,
|
|
IsEnabled = isEnabled1
|
|
};
|
|
|
|
var created = service.CreateAsync(createRequest).GetAwaiter().GetResult();
|
|
|
|
// Update the announcement
|
|
var updateRequest = new UpdateAnnouncementRequest
|
|
{
|
|
UserAvatar = "http://test.com/avatar2.jpg",
|
|
UserName = userName2,
|
|
PrizeLevel = prizeLevel2,
|
|
PrizeName = prizeName2,
|
|
Sort = actualSort2,
|
|
IsEnabled = isEnabled2
|
|
};
|
|
|
|
service.UpdateAsync(created.Id, updateRequest).GetAwaiter().GetResult();
|
|
|
|
// Read it back
|
|
var retrieved = service.GetByIdAsync(created.Id).GetAwaiter().GetResult();
|
|
|
|
// Verify updates are reflected
|
|
return retrieved.UserName == userName2 &&
|
|
retrieved.PrizeLevel == prizeLevel2 &&
|
|
retrieved.PrizeName == prizeName2 &&
|
|
retrieved.Sort == actualSort2 &&
|
|
retrieved.IsEnabled == isEnabled2 &&
|
|
retrieved.UserAvatar == "http://test.com/avatar2.jpg";
|
|
}
|
|
finally
|
|
{
|
|
dbContext.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// **Feature: homepage-prize-announcement, Property 1: CRUD Round-Trip Consistency**
|
|
/// For any valid announcement, deleting it should make it no longer retrievable.
|
|
/// **Validates: Requirements 1.1, 1.2, 1.3, 2.4**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool CrudRoundTrip_Delete_ShouldMakeUnretrievable(PositiveInt seed)
|
|
{
|
|
var userName = GenerateValidString("User", seed.Get);
|
|
var prizeLevel = GenerateValidString("Level", seed.Get);
|
|
var prizeName = GenerateValidString("Prize", seed.Get);
|
|
|
|
var (dbContext, service) = CreateService();
|
|
|
|
try
|
|
{
|
|
// Create announcement
|
|
var createRequest = new CreateAnnouncementRequest
|
|
{
|
|
UserName = userName,
|
|
PrizeLevel = prizeLevel,
|
|
PrizeName = prizeName,
|
|
Sort = 0,
|
|
IsEnabled = true
|
|
};
|
|
|
|
var created = service.CreateAsync(createRequest).GetAwaiter().GetResult();
|
|
var createdId = created.Id;
|
|
|
|
// Delete the announcement
|
|
var deleteResult = service.DeleteAsync(createdId).GetAwaiter().GetResult();
|
|
|
|
if (!deleteResult)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Try to retrieve it - should throw NotFound exception
|
|
try
|
|
{
|
|
service.GetByIdAsync(createdId).GetAwaiter().GetResult();
|
|
return false; // Should not reach here
|
|
}
|
|
catch (BusinessException ex)
|
|
{
|
|
return ex.Code == BusinessErrorCodes.NotFound;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
dbContext.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// **Feature: homepage-prize-announcement, Property 1: CRUD Round-Trip Consistency**
|
|
/// Partial updates should only modify specified fields, leaving others unchanged.
|
|
/// **Validates: Requirements 1.1, 1.2, 1.3, 2.4**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool CrudRoundTrip_PartialUpdate_ShouldOnlyModifySpecifiedFields(
|
|
PositiveInt seed,
|
|
PositiveInt newSort)
|
|
{
|
|
var userName = GenerateValidString("User", seed.Get);
|
|
var prizeLevel = GenerateValidString("Level", seed.Get);
|
|
var prizeName = GenerateValidString("Prize", seed.Get);
|
|
var originalSort = seed.Get % 500;
|
|
var updatedSort = (newSort.Get % 500) + 500; // Ensure different value
|
|
|
|
var (dbContext, service) = CreateService();
|
|
|
|
try
|
|
{
|
|
// Create announcement
|
|
var createRequest = new CreateAnnouncementRequest
|
|
{
|
|
UserAvatar = "http://test.com/original.jpg",
|
|
UserName = userName,
|
|
PrizeLevel = prizeLevel,
|
|
PrizeName = prizeName,
|
|
Sort = originalSort,
|
|
IsEnabled = true
|
|
};
|
|
|
|
var created = service.CreateAsync(createRequest).GetAwaiter().GetResult();
|
|
|
|
// Partial update - only update Sort
|
|
var updateRequest = new UpdateAnnouncementRequest
|
|
{
|
|
Sort = updatedSort
|
|
};
|
|
|
|
service.UpdateAsync(created.Id, updateRequest).GetAwaiter().GetResult();
|
|
|
|
// Read it back
|
|
var retrieved = service.GetByIdAsync(created.Id).GetAwaiter().GetResult();
|
|
|
|
// Verify only Sort changed, other fields remain the same
|
|
return retrieved.Sort == updatedSort &&
|
|
retrieved.UserName == userName &&
|
|
retrieved.PrizeLevel == prizeLevel &&
|
|
retrieved.PrizeName == prizeName &&
|
|
retrieved.UserAvatar == "http://test.com/original.jpg" &&
|
|
retrieved.IsEnabled == true;
|
|
}
|
|
finally
|
|
{
|
|
dbContext.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// **Feature: homepage-prize-announcement, Property 1: CRUD Round-Trip Consistency**
|
|
/// Multiple CRUD operations should maintain data consistency.
|
|
/// **Validates: Requirements 1.1, 1.2, 1.3, 2.4**
|
|
/// </summary>
|
|
[Property(MaxTest = 50)]
|
|
public bool CrudRoundTrip_MultipleCrudOperations_ShouldMaintainConsistency(PositiveInt operationCount)
|
|
{
|
|
var actualCount = Math.Max(2, operationCount.Get % 5 + 2);
|
|
|
|
var (dbContext, service) = CreateService();
|
|
|
|
try
|
|
{
|
|
var createdIds = new List<int>();
|
|
|
|
// Create multiple announcements
|
|
for (int i = 0; i < actualCount; i++)
|
|
{
|
|
var createRequest = new CreateAnnouncementRequest
|
|
{
|
|
UserName = $"User_{i}",
|
|
PrizeLevel = $"Level_{i}",
|
|
PrizeName = $"Prize_{i}",
|
|
Sort = i,
|
|
IsEnabled = true
|
|
};
|
|
|
|
var created = service.CreateAsync(createRequest).GetAwaiter().GetResult();
|
|
createdIds.Add(created.Id);
|
|
}
|
|
|
|
// Update each announcement
|
|
for (int i = 0; i < actualCount; i++)
|
|
{
|
|
var updateRequest = new UpdateAnnouncementRequest
|
|
{
|
|
UserName = $"UpdatedUser_{i}",
|
|
Sort = i + 100
|
|
};
|
|
|
|
service.UpdateAsync(createdIds[i], updateRequest).GetAwaiter().GetResult();
|
|
}
|
|
|
|
// Verify all updates
|
|
for (int i = 0; i < actualCount; i++)
|
|
{
|
|
var retrieved = service.GetByIdAsync(createdIds[i]).GetAwaiter().GetResult();
|
|
if (retrieved.UserName != $"UpdatedUser_{i}" || retrieved.Sort != i + 100)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Delete half of them
|
|
for (int i = 0; i < actualCount / 2; i++)
|
|
{
|
|
service.DeleteAsync(createdIds[i]).GetAwaiter().GetResult();
|
|
}
|
|
|
|
// Verify deleted ones are not retrievable
|
|
for (int i = 0; i < actualCount / 2; i++)
|
|
{
|
|
try
|
|
{
|
|
service.GetByIdAsync(createdIds[i]).GetAwaiter().GetResult();
|
|
return false; // Should have thrown
|
|
}
|
|
catch (BusinessException ex)
|
|
{
|
|
if (ex.Code != BusinessErrorCodes.NotFound)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify remaining ones are still retrievable
|
|
for (int i = actualCount / 2; i < actualCount; i++)
|
|
{
|
|
var retrieved = service.GetByIdAsync(createdIds[i]).GetAwaiter().GetResult();
|
|
if (retrieved.UserName != $"UpdatedUser_{i}")
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
finally
|
|
{
|
|
dbContext.Dispose();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Property 4: Required Field Validation
|
|
|
|
/// <summary>
|
|
/// **Feature: homepage-prize-announcement, Property 4: Required Field Validation**
|
|
/// For any create request where UserName is empty or whitespace-only,
|
|
/// the system should reject the request and return a validation error.
|
|
/// **Validates: Requirements 1.5**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool RequiredFieldValidation_EmptyUserName_ShouldRejectCreate(PositiveInt seed)
|
|
{
|
|
var prizeLevel = GenerateValidString("Level", seed.Get);
|
|
var prizeName = GenerateValidString("Prize", seed.Get);
|
|
|
|
var (dbContext, service) = CreateService();
|
|
|
|
try
|
|
{
|
|
var emptyUserNames = new[] { "", " ", "\t", "\n", " \t\n " };
|
|
|
|
foreach (var emptyUserName in emptyUserNames)
|
|
{
|
|
var createRequest = new CreateAnnouncementRequest
|
|
{
|
|
UserName = emptyUserName,
|
|
PrizeLevel = prizeLevel,
|
|
PrizeName = prizeName,
|
|
Sort = 0,
|
|
IsEnabled = true
|
|
};
|
|
|
|
try
|
|
{
|
|
service.CreateAsync(createRequest).GetAwaiter().GetResult();
|
|
return false; // Should have thrown
|
|
}
|
|
catch (BusinessException ex)
|
|
{
|
|
if (ex.Code != BusinessErrorCodes.ValidationFailed)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify database is not modified
|
|
var count = dbContext.PrizeAnnouncements.Count();
|
|
return count == 0;
|
|
}
|
|
finally
|
|
{
|
|
dbContext.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// **Feature: homepage-prize-announcement, Property 4: Required Field Validation**
|
|
/// For any create request where PrizeLevel is empty or whitespace-only,
|
|
/// the system should reject the request and return a validation error.
|
|
/// **Validates: Requirements 1.5**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool RequiredFieldValidation_EmptyPrizeLevel_ShouldRejectCreate(PositiveInt seed)
|
|
{
|
|
var userName = GenerateValidString("User", seed.Get);
|
|
var prizeName = GenerateValidString("Prize", seed.Get);
|
|
|
|
var (dbContext, service) = CreateService();
|
|
|
|
try
|
|
{
|
|
var emptyPrizeLevels = new[] { "", " ", "\t", "\n", " \t\n " };
|
|
|
|
foreach (var emptyPrizeLevel in emptyPrizeLevels)
|
|
{
|
|
var createRequest = new CreateAnnouncementRequest
|
|
{
|
|
UserName = userName,
|
|
PrizeLevel = emptyPrizeLevel,
|
|
PrizeName = prizeName,
|
|
Sort = 0,
|
|
IsEnabled = true
|
|
};
|
|
|
|
try
|
|
{
|
|
service.CreateAsync(createRequest).GetAwaiter().GetResult();
|
|
return false; // Should have thrown
|
|
}
|
|
catch (BusinessException ex)
|
|
{
|
|
if (ex.Code != BusinessErrorCodes.ValidationFailed)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify database is not modified
|
|
var count = dbContext.PrizeAnnouncements.Count();
|
|
return count == 0;
|
|
}
|
|
finally
|
|
{
|
|
dbContext.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// **Feature: homepage-prize-announcement, Property 4: Required Field Validation**
|
|
/// For any create request where PrizeName is empty or whitespace-only,
|
|
/// the system should reject the request and return a validation error.
|
|
/// **Validates: Requirements 1.5**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool RequiredFieldValidation_EmptyPrizeName_ShouldRejectCreate(PositiveInt seed)
|
|
{
|
|
var userName = GenerateValidString("User", seed.Get);
|
|
var prizeLevel = GenerateValidString("Level", seed.Get);
|
|
|
|
var (dbContext, service) = CreateService();
|
|
|
|
try
|
|
{
|
|
var emptyPrizeNames = new[] { "", " ", "\t", "\n", " \t\n " };
|
|
|
|
foreach (var emptyPrizeName in emptyPrizeNames)
|
|
{
|
|
var createRequest = new CreateAnnouncementRequest
|
|
{
|
|
UserName = userName,
|
|
PrizeLevel = prizeLevel,
|
|
PrizeName = emptyPrizeName,
|
|
Sort = 0,
|
|
IsEnabled = true
|
|
};
|
|
|
|
try
|
|
{
|
|
service.CreateAsync(createRequest).GetAwaiter().GetResult();
|
|
return false; // Should have thrown
|
|
}
|
|
catch (BusinessException ex)
|
|
{
|
|
if (ex.Code != BusinessErrorCodes.ValidationFailed)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify database is not modified
|
|
var count = dbContext.PrizeAnnouncements.Count();
|
|
return count == 0;
|
|
}
|
|
finally
|
|
{
|
|
dbContext.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// **Feature: homepage-prize-announcement, Property 4: Required Field Validation**
|
|
/// For any update request where UserName is set to empty or whitespace-only,
|
|
/// the system should reject the request and return a validation error without modifying the database.
|
|
/// **Validates: Requirements 1.5**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool RequiredFieldValidation_EmptyUserName_ShouldRejectUpdate(PositiveInt seed)
|
|
{
|
|
var userName = GenerateValidString("User", seed.Get);
|
|
var prizeLevel = GenerateValidString("Level", seed.Get);
|
|
var prizeName = GenerateValidString("Prize", seed.Get);
|
|
|
|
var (dbContext, service) = CreateService();
|
|
|
|
try
|
|
{
|
|
// First create a valid announcement
|
|
var createRequest = new CreateAnnouncementRequest
|
|
{
|
|
UserName = userName,
|
|
PrizeLevel = prizeLevel,
|
|
PrizeName = prizeName,
|
|
Sort = 0,
|
|
IsEnabled = true
|
|
};
|
|
|
|
var created = service.CreateAsync(createRequest).GetAwaiter().GetResult();
|
|
|
|
var emptyUserNames = new[] { "", " ", "\t", "\n" };
|
|
|
|
foreach (var emptyUserName in emptyUserNames)
|
|
{
|
|
var updateRequest = new UpdateAnnouncementRequest
|
|
{
|
|
UserName = emptyUserName
|
|
};
|
|
|
|
try
|
|
{
|
|
service.UpdateAsync(created.Id, updateRequest).GetAwaiter().GetResult();
|
|
return false; // Should have thrown
|
|
}
|
|
catch (BusinessException ex)
|
|
{
|
|
if (ex.Code != BusinessErrorCodes.ValidationFailed)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify original data is not modified
|
|
var retrieved = service.GetByIdAsync(created.Id).GetAwaiter().GetResult();
|
|
return retrieved.UserName == userName;
|
|
}
|
|
finally
|
|
{
|
|
dbContext.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// **Feature: homepage-prize-announcement, Property 4: Required Field Validation**
|
|
/// For any update request where PrizeLevel is set to empty or whitespace-only,
|
|
/// the system should reject the request and return a validation error without modifying the database.
|
|
/// **Validates: Requirements 1.5**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool RequiredFieldValidation_EmptyPrizeLevel_ShouldRejectUpdate(PositiveInt seed)
|
|
{
|
|
var userName = GenerateValidString("User", seed.Get);
|
|
var prizeLevel = GenerateValidString("Level", seed.Get);
|
|
var prizeName = GenerateValidString("Prize", seed.Get);
|
|
|
|
var (dbContext, service) = CreateService();
|
|
|
|
try
|
|
{
|
|
// First create a valid announcement
|
|
var createRequest = new CreateAnnouncementRequest
|
|
{
|
|
UserName = userName,
|
|
PrizeLevel = prizeLevel,
|
|
PrizeName = prizeName,
|
|
Sort = 0,
|
|
IsEnabled = true
|
|
};
|
|
|
|
var created = service.CreateAsync(createRequest).GetAwaiter().GetResult();
|
|
|
|
var emptyPrizeLevels = new[] { "", " ", "\t", "\n" };
|
|
|
|
foreach (var emptyPrizeLevel in emptyPrizeLevels)
|
|
{
|
|
var updateRequest = new UpdateAnnouncementRequest
|
|
{
|
|
PrizeLevel = emptyPrizeLevel
|
|
};
|
|
|
|
try
|
|
{
|
|
service.UpdateAsync(created.Id, updateRequest).GetAwaiter().GetResult();
|
|
return false; // Should have thrown
|
|
}
|
|
catch (BusinessException ex)
|
|
{
|
|
if (ex.Code != BusinessErrorCodes.ValidationFailed)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify original data is not modified
|
|
var retrieved = service.GetByIdAsync(created.Id).GetAwaiter().GetResult();
|
|
return retrieved.PrizeLevel == prizeLevel;
|
|
}
|
|
finally
|
|
{
|
|
dbContext.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// **Feature: homepage-prize-announcement, Property 4: Required Field Validation**
|
|
/// For any update request where PrizeName is set to empty or whitespace-only,
|
|
/// the system should reject the request and return a validation error without modifying the database.
|
|
/// **Validates: Requirements 1.5**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool RequiredFieldValidation_EmptyPrizeName_ShouldRejectUpdate(PositiveInt seed)
|
|
{
|
|
var userName = GenerateValidString("User", seed.Get);
|
|
var prizeLevel = GenerateValidString("Level", seed.Get);
|
|
var prizeName = GenerateValidString("Prize", seed.Get);
|
|
|
|
var (dbContext, service) = CreateService();
|
|
|
|
try
|
|
{
|
|
// First create a valid announcement
|
|
var createRequest = new CreateAnnouncementRequest
|
|
{
|
|
UserName = userName,
|
|
PrizeLevel = prizeLevel,
|
|
PrizeName = prizeName,
|
|
Sort = 0,
|
|
IsEnabled = true
|
|
};
|
|
|
|
var created = service.CreateAsync(createRequest).GetAwaiter().GetResult();
|
|
|
|
var emptyPrizeNames = new[] { "", " ", "\t", "\n" };
|
|
|
|
foreach (var emptyPrizeName in emptyPrizeNames)
|
|
{
|
|
var updateRequest = new UpdateAnnouncementRequest
|
|
{
|
|
PrizeName = emptyPrizeName
|
|
};
|
|
|
|
try
|
|
{
|
|
service.UpdateAsync(created.Id, updateRequest).GetAwaiter().GetResult();
|
|
return false; // Should have thrown
|
|
}
|
|
catch (BusinessException ex)
|
|
{
|
|
if (ex.Code != BusinessErrorCodes.ValidationFailed)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify original data is not modified
|
|
var retrieved = service.GetByIdAsync(created.Id).GetAwaiter().GetResult();
|
|
return retrieved.PrizeName == prizeName;
|
|
}
|
|
finally
|
|
{
|
|
dbContext.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// **Feature: homepage-prize-announcement, Property 4: Required Field Validation**
|
|
/// For any create request with all required fields empty,
|
|
/// the system should reject the request without modifying the database.
|
|
/// **Validates: Requirements 1.5**
|
|
/// </summary>
|
|
[Property(MaxTest = 50)]
|
|
public bool RequiredFieldValidation_AllFieldsEmpty_ShouldRejectCreate(PositiveInt seed)
|
|
{
|
|
var (dbContext, service) = CreateService();
|
|
|
|
try
|
|
{
|
|
var createRequest = new CreateAnnouncementRequest
|
|
{
|
|
UserName = "",
|
|
PrizeLevel = "",
|
|
PrizeName = "",
|
|
Sort = 0,
|
|
IsEnabled = true
|
|
};
|
|
|
|
try
|
|
{
|
|
service.CreateAsync(createRequest).GetAwaiter().GetResult();
|
|
return false; // Should have thrown
|
|
}
|
|
catch (BusinessException ex)
|
|
{
|
|
if (ex.Code != BusinessErrorCodes.ValidationFailed)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Verify database is not modified
|
|
var count = dbContext.PrizeAnnouncements.Count();
|
|
return count == 0;
|
|
}
|
|
finally
|
|
{
|
|
dbContext.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// **Feature: homepage-prize-announcement, Property 4: Required Field Validation**
|
|
/// Valid requests with non-empty required fields should succeed.
|
|
/// **Validates: Requirements 1.5**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool RequiredFieldValidation_ValidFields_ShouldSucceed(
|
|
NonEmptyString userName,
|
|
NonEmptyString prizeLevel,
|
|
NonEmptyString prizeName)
|
|
{
|
|
var actualUserName = userName.Get.Trim();
|
|
var actualPrizeLevel = prizeLevel.Get.Trim();
|
|
var actualPrizeName = prizeName.Get.Trim();
|
|
|
|
// Skip if any required field becomes empty after trim
|
|
if (string.IsNullOrWhiteSpace(actualUserName) ||
|
|
string.IsNullOrWhiteSpace(actualPrizeLevel) ||
|
|
string.IsNullOrWhiteSpace(actualPrizeName))
|
|
{
|
|
return true; // Skip this test case
|
|
}
|
|
|
|
var (dbContext, service) = CreateService();
|
|
|
|
try
|
|
{
|
|
var createRequest = new CreateAnnouncementRequest
|
|
{
|
|
UserName = actualUserName,
|
|
PrizeLevel = actualPrizeLevel,
|
|
PrizeName = actualPrizeName,
|
|
Sort = 0,
|
|
IsEnabled = true
|
|
};
|
|
|
|
var created = service.CreateAsync(createRequest).GetAwaiter().GetResult();
|
|
|
|
// Verify creation succeeded
|
|
return created.Id > 0 &&
|
|
created.UserName == actualUserName &&
|
|
created.PrizeLevel == actualPrizeLevel &&
|
|
created.PrizeName == actualPrizeName;
|
|
}
|
|
catch (BusinessException)
|
|
{
|
|
return false; // Should not throw for valid data
|
|
}
|
|
finally
|
|
{
|
|
dbContext.Dispose();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|