feat(web): define portfolio contracts and validation
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
using GmRelay.Web.Services.Portfolio;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Web;
|
||||
|
||||
public sealed class PortfolioContractsTests
|
||||
{
|
||||
[Fact]
|
||||
public void PublicPortfolioCard_ShouldExposeOnlySanitizedPublicProperties()
|
||||
{
|
||||
AssertNoForbiddenPropertyNames<PublicPortfolioCard>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublicPortfolioGame_ShouldExposeOnlySanitizedPublicProperties()
|
||||
{
|
||||
AssertNoForbiddenPropertyNames<PublicPortfolioGame>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublicPortfolioMaster_ShouldExposeOnlySanitizedPublicProperties()
|
||||
{
|
||||
AssertNoForbiddenPropertyNames<PublicPortfolioMaster>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublicPortfolioReview_ShouldExposeOnlySanitizedPublicProperties()
|
||||
{
|
||||
AssertNoForbiddenPropertyNames<PublicPortfolioReview>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublicPortfolioCard_ShouldExposeExpectedProperties()
|
||||
{
|
||||
var names = typeof(PublicPortfolioCard).GetProperties().Select(p => p.Name).ToArray();
|
||||
|
||||
Assert.Contains("Slug", names);
|
||||
Assert.Contains("Title", names);
|
||||
Assert.Contains("CoverPath", names);
|
||||
Assert.Contains("System", names);
|
||||
Assert.Contains("Format", names);
|
||||
Assert.Contains("CompletedAt", names);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublicPortfolioGame_ShouldExposeExpectedProperties()
|
||||
{
|
||||
var names = typeof(PublicPortfolioGame).GetProperties().Select(p => p.Name).ToArray();
|
||||
|
||||
Assert.Contains("Slug", names);
|
||||
Assert.Contains("Title", names);
|
||||
Assert.Contains("Description", names);
|
||||
Assert.Contains("CoverPath", names);
|
||||
Assert.Contains("System", names);
|
||||
Assert.Contains("Format", names);
|
||||
Assert.Contains("CompletedAt", names);
|
||||
Assert.Contains("ClubName", names);
|
||||
Assert.Contains("ClubSlug", names);
|
||||
Assert.Contains("Masters", names);
|
||||
Assert.Contains("Reviews", names);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublicPortfolioMaster_ShouldExposeExpectedProperties()
|
||||
{
|
||||
var names = typeof(PublicPortfolioMaster).GetProperties().Select(p => p.Name).ToArray();
|
||||
|
||||
Assert.Contains("Slug", names);
|
||||
Assert.Contains("DisplayName", names);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublicPortfolioReview_ShouldExposeExpectedProperties()
|
||||
{
|
||||
var names = typeof(PublicPortfolioReview).GetProperties().Select(p => p.Name).ToArray();
|
||||
|
||||
Assert.Contains("AuthorDisplayName", names);
|
||||
Assert.Contains("Body", names);
|
||||
Assert.Contains("CreatedAt", names);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IPortfolioStore_ShouldExposeAllRequiredMethods()
|
||||
{
|
||||
var interfaceType = typeof(IPortfolioStore);
|
||||
var methodNames = interfaceType.GetMethods().Select(m => m.Name).ToArray();
|
||||
|
||||
Assert.Contains("GetPublicPortfolioGamesForMasterAsync", methodNames);
|
||||
Assert.Contains("GetPublicPortfolioGamesForClubAsync", methodNames);
|
||||
Assert.Contains("GetPublicPortfolioGameBySlugAsync", methodNames);
|
||||
Assert.Contains("GetPortfolioGamesForGroupAsync", methodNames);
|
||||
Assert.Contains("GetPortfolioGameGroupIdAsync", methodNames);
|
||||
Assert.Contains("GetPortfolioGameForManagementAsync", methodNames);
|
||||
Assert.Contains("GetEligibleCompletedSessionsAsync", methodNames);
|
||||
Assert.Contains("GetPortfolioMasterOptionsAsync", methodNames);
|
||||
Assert.Contains("CreatePortfolioDraftAsync", methodNames);
|
||||
Assert.Contains("UpdatePortfolioDraftAsync", methodNames);
|
||||
Assert.Contains("SetPortfolioCoverAsync", methodNames);
|
||||
Assert.Contains("DeletePortfolioGameAsync", methodNames);
|
||||
Assert.Contains("SetPortfolioPublicationAsync", methodNames);
|
||||
Assert.Contains("ModeratePortfolioReviewAsync", methodNames);
|
||||
Assert.Contains("GetReviewSubmissionStateAsync", methodNames);
|
||||
Assert.Contains("SubmitPortfolioReviewAsync", methodNames);
|
||||
}
|
||||
|
||||
private static void AssertNoForbiddenPropertyNames<T>()
|
||||
{
|
||||
var forbidden = new[]
|
||||
{
|
||||
"Id", "External", "Telegram", "Discord", "Moderator",
|
||||
"StorageKey", "PhysicalPath", "JoinLink", "Session"
|
||||
};
|
||||
|
||||
var names = typeof(T).GetProperties().Select(p => p.Name).ToArray();
|
||||
|
||||
foreach (var forbiddenFragment in forbidden)
|
||||
{
|
||||
Assert.DoesNotContain(names, name => name.Contains(forbiddenFragment, StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using GmRelay.Web.Services.Portfolio;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Web;
|
||||
|
||||
public sealed class PortfolioValidationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(" Dragon Heist ", "dragon-heist")]
|
||||
[InlineData("dragon_heist", "dragon-heist")]
|
||||
[InlineData("Dragon Heist", "dragon-heist")]
|
||||
[InlineData("dragon---heist", "dragon-heist")]
|
||||
[InlineData("dragon-heist-", "dragon-heist")]
|
||||
[InlineData("DRAGON-Heist", "dragon-heist")]
|
||||
public void NormalizeSlug_ShouldReturnCanonicalSlug(string input, string expected)
|
||||
{
|
||||
Assert.Equal(expected, PortfolioValidation.NormalizeSlug(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("ab")]
|
||||
[InlineData("spaces are fine after normalization but this slug is intentionally far too long to be accepted because it exceeds the maximum portfolio slug size of one hundred and sixty characters")]
|
||||
[InlineData("кириллица")]
|
||||
[InlineData("hello world!")]
|
||||
[InlineData("---")]
|
||||
public void NormalizeSlug_ShouldRejectInvalidSlug(string input)
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() => PortfolioValidation.NormalizeSlug(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void NormalizeReviewBody_ShouldRejectBlankText(string? body)
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() => PortfolioValidation.NormalizeReviewBody(body));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("This is a perfectly valid review body for the portfolio entry that should pass.")]
|
||||
[InlineData(" Another valid review body that meets the minimum length requirement. ")]
|
||||
public void NormalizeReviewBody_ShouldTrimAndAcceptValidText(string body)
|
||||
{
|
||||
Assert.Equal(body.Trim(), PortfolioValidation.NormalizeReviewBody(body));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(" Hello World ")]
|
||||
[InlineData("abc")]
|
||||
public void NormalizeTitle_ShouldTrimAndAcceptValidText(string title)
|
||||
{
|
||||
Assert.Equal(title.Trim(), PortfolioValidation.NormalizeTitle(title));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("a")]
|
||||
public void NormalizeTitle_ShouldRejectTooShort(string title)
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() => PortfolioValidation.NormalizeTitle(title));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeDescription_ShouldReturnNullForWhitespace()
|
||||
{
|
||||
Assert.Null(PortfolioValidation.NormalizeDescription(null));
|
||||
Assert.Null(PortfolioValidation.NormalizeDescription(""));
|
||||
Assert.Null(PortfolioValidation.NormalizeDescription(" "));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeDescription_ShouldTrimAndAcceptValidText()
|
||||
{
|
||||
Assert.Equal("hello", PortfolioValidation.NormalizeDescription(" hello "));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Online")]
|
||||
[InlineData("Offline")]
|
||||
[InlineData("Hybrid")]
|
||||
public void NormalizeFormat_ShouldAcceptKnownValues(string format)
|
||||
{
|
||||
Assert.Equal(format, PortfolioValidation.NormalizeFormat(format));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void NormalizeFormat_ShouldReturnNullForBlank(string? format)
|
||||
{
|
||||
Assert.Null(PortfolioValidation.NormalizeFormat(format));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("online")]
|
||||
[InlineData("VTT")]
|
||||
public void NormalizeFormat_ShouldRejectUnknownValues(string format)
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() => PortfolioValidation.NormalizeFormat(format));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user