From ec71e14a8345fda6ad444af4925904e209116fb4 Mon Sep 17 00:00:00 2001 From: ZenonEl <165126589+ZenonEl@users.noreply.github.com> Date: Sat, 28 Mar 2026 07:54:33 +0400 Subject: [PATCH] feat(tests): add unit test project with Result, UserSettings, Error, and Session tests closes #12 --- .../Domain/ErrorTests.cs | 53 ++++++++ .../Domain/ResultTests.cs | 119 ++++++++++++++++++ .../Domain/UserSettingsTests.cs | 106 ++++++++++++++++ .../Sessions/UserSessionManagerTests.cs | 86 +++++++++++++ .../TelegramMediaRelayBot.Tests.csproj | 28 +++++ TelegramMediaRelayBot.csproj | 1 + 6 files changed, 393 insertions(+) create mode 100644 TelegramMediaRelayBot.Tests/Domain/ErrorTests.cs create mode 100644 TelegramMediaRelayBot.Tests/Domain/ResultTests.cs create mode 100644 TelegramMediaRelayBot.Tests/Domain/UserSettingsTests.cs create mode 100644 TelegramMediaRelayBot.Tests/Sessions/UserSessionManagerTests.cs create mode 100644 TelegramMediaRelayBot.Tests/TelegramMediaRelayBot.Tests.csproj diff --git a/TelegramMediaRelayBot.Tests/Domain/ErrorTests.cs b/TelegramMediaRelayBot.Tests/Domain/ErrorTests.cs new file mode 100644 index 0000000..dce2958 --- /dev/null +++ b/TelegramMediaRelayBot.Tests/Domain/ErrorTests.cs @@ -0,0 +1,53 @@ +using TelegramMediaRelayBot.Domain.Common; + +namespace TelegramMediaRelayBot.Tests.Domain; + +public class ErrorTests +{ + [Fact] + public void Validation_ShouldCreateValidationError() + { + var error = Error.Validation("val.001", "invalid input"); + + Assert.Equal("val.001", error.Code); + Assert.Equal("invalid input", error.Message); + Assert.Equal(ErrorType.Validation, error.Type); + } + + [Fact] + public void NotFound_ShouldCreateNotFoundError() + { + var error = Error.NotFound("nf.001", "resource missing"); + + Assert.Equal(ErrorType.NotFound, error.Type); + } + + [Fact] + public void Infrastructure_ShouldCreateInfrastructureError() + { + var error = Error.Infrastructure("infra.001", "db down"); + + Assert.Equal(ErrorType.Infrastructure, error.Type); + Assert.Equal("infra.001", error.Code); + Assert.Equal("db down", error.Message); + } + + [Fact] + public void EqualErrors_ShouldBeEqual() + { + var a = Error.Validation("code", "msg"); + var b = Error.Validation("code", "msg"); + + Assert.Equal(a, b); + Assert.True(a == b); + } + + [Fact] + public void DifferentErrors_ShouldNotBeEqual() + { + var a = Error.Validation("code", "msg"); + var b = Error.NotFound("code", "msg"); + + Assert.NotEqual(a, b); + } +} diff --git a/TelegramMediaRelayBot.Tests/Domain/ResultTests.cs b/TelegramMediaRelayBot.Tests/Domain/ResultTests.cs new file mode 100644 index 0000000..26a8dfd --- /dev/null +++ b/TelegramMediaRelayBot.Tests/Domain/ResultTests.cs @@ -0,0 +1,119 @@ +using TelegramMediaRelayBot.Domain.Common; + +namespace TelegramMediaRelayBot.Tests.Domain; + +public class ResultTests +{ + [Fact] + public void Success_ShouldSetValueAndIsSuccess() + { + var result = Result.Success(42); + + Assert.True(result.IsSuccess); + Assert.False(result.IsFailure); + Assert.Equal(42, result.Value); + Assert.Null(result.Error); + } + + [Fact] + public void Failure_ShouldSetErrorAndIsFailure() + { + var error = Error.Validation("test", "fail"); + var result = Result.Failure(error); + + Assert.True(result.IsFailure); + Assert.False(result.IsSuccess); + Assert.Equal(error, result.Error); + } + + [Fact] + public void Map_OnSuccess_ShouldTransformValue() + { + var result = Result.Success(10); + + var mapped = result.Map(x => x * 2); + + Assert.True(mapped.IsSuccess); + Assert.Equal(20, mapped.Value); + } + + [Fact] + public void Map_OnFailure_ShouldPropagateError() + { + var error = Error.NotFound("nf", "not found"); + var result = Result.Failure(error); + + var mapped = result.Map(x => x.ToString()); + + Assert.True(mapped.IsFailure); + Assert.Equal(error, mapped.Error); + } + + [Fact] + public void Bind_OnSuccess_ShouldChainResults() + { + var result = Result.Success(5); + + var bound = result.Bind(x => + x > 0 ? Result.Success(x.ToString()) : Result.Failure(Error.Validation("neg", "negative"))); + + Assert.True(bound.IsSuccess); + Assert.Equal("5", bound.Value); + } + + [Fact] + public void Bind_OnFailure_ShouldPropagateError() + { + var error = Error.Infrastructure("err", "infra error"); + var result = Result.Failure(error); + + var bound = result.Bind(x => Result.Success(x.ToString())); + + Assert.True(bound.IsFailure); + Assert.Equal(error, bound.Error); + } + + [Fact] + public void Match_OnSuccess_ShouldCallOnSuccess() + { + var result = Result.Success(7); + + var output = result.Match( + onSuccess: v => $"ok:{v}", + onFailure: e => $"err:{e.Code}"); + + Assert.Equal("ok:7", output); + } + + [Fact] + public void Match_OnFailure_ShouldCallOnFailure() + { + var error = Error.External("ext", "external"); + var result = Result.Failure(error); + + var output = result.Match( + onSuccess: v => $"ok:{v}", + onFailure: e => $"err:{e.Code}"); + + Assert.Equal("err:ext", output); + } + + [Fact] + public void StaticSuccess_ShouldCreateSuccessResult() + { + var result = Result.Success("hello"); + + Assert.True(result.IsSuccess); + Assert.Equal("hello", result.Value); + } + + [Fact] + public void StaticFailure_ShouldCreateFailureResult() + { + var error = Error.Conflict("dup", "duplicate"); + var result = Result.Failure(error); + + Assert.True(result.IsFailure); + Assert.Equal(error, result.Error); + } +} diff --git a/TelegramMediaRelayBot.Tests/Domain/UserSettingsTests.cs b/TelegramMediaRelayBot.Tests/Domain/UserSettingsTests.cs new file mode 100644 index 0000000..331f17a --- /dev/null +++ b/TelegramMediaRelayBot.Tests/Domain/UserSettingsTests.cs @@ -0,0 +1,106 @@ +using TelegramMediaRelayBot.Domain.Models; + +namespace TelegramMediaRelayBot.Tests.Domain; + +public class UserSettingsTests +{ + [Fact] + public void RoundTrip_ShouldPreserveValues() + { + var settings = new UserSettings(); + settings.Distribution.DefaultAction = "send_to_all"; + settings.Distribution.AutoSendDelaySeconds = 60; + settings.Privacy.InboxEnabled = true; + + var json = settings.ToJson(); + var restored = UserSettings.FromJson(json); + + Assert.Equal("send_to_all", restored.Distribution.DefaultAction); + Assert.Equal(60, restored.Distribution.AutoSendDelaySeconds); + Assert.True(restored.Privacy.InboxEnabled); + } + + [Fact] + public void DefaultValues_ShouldBeCorrect() + { + var settings = new UserSettings(); + + Assert.Equal("send_only_to_me", settings.Distribution.DefaultAction); + Assert.Equal("", settings.Distribution.DefaultActionCondition); + Assert.Equal(30, settings.Distribution.AutoSendDelaySeconds); + Assert.Empty(settings.Distribution.TargetGroupIds); + Assert.Empty(settings.Distribution.TargetContactIds); + Assert.False(settings.Privacy.InboxEnabled); + Assert.True(settings.Privacy.AllowContentForwarding); + Assert.Equal("everyone", settings.Privacy.WhoCanFindMe); + } + + [Fact] + public void FromJson_EmptyString_ShouldReturnDefaults() + { + var settings = UserSettings.FromJson(""); + + Assert.Equal("send_only_to_me", settings.Distribution.DefaultAction); + Assert.Equal(30, settings.Distribution.AutoSendDelaySeconds); + } + + [Fact] + public void FromJson_Null_ShouldReturnDefaults() + { + var settings = UserSettings.FromJson(null!); + + Assert.NotNull(settings); + Assert.NotNull(settings.Distribution); + Assert.NotNull(settings.Privacy); + } + + [Fact] + public void FromJson_InvalidJson_ShouldReturnDefaults() + { + var settings = UserSettings.FromJson("{broken json!!!"); + + Assert.NotNull(settings); + Assert.Equal("send_only_to_me", settings.Distribution.DefaultAction); + } + + [Fact] + public void NestedSiteFilter_ShouldRoundTrip() + { + var settings = new UserSettings(); + settings.Privacy.SiteFilter.Enabled = true; + settings.Privacy.SiteFilter.FilterType = "blocklist"; + settings.Privacy.SiteFilter.BlockedDomains.Add("example.com"); + + var json = settings.ToJson(); + var restored = UserSettings.FromJson(json); + + Assert.True(restored.Privacy.SiteFilter.Enabled); + Assert.Equal("blocklist", restored.Privacy.SiteFilter.FilterType); + Assert.Single(restored.Privacy.SiteFilter.BlockedDomains); + Assert.Equal("example.com", restored.Privacy.SiteFilter.BlockedDomains[0]); + } + + [Fact] + public void DistributionTargets_ShouldRoundTrip() + { + var settings = new UserSettings(); + settings.Distribution.TargetGroupIds.AddRange([1, 2, 3]); + settings.Distribution.TargetContactIds.AddRange([10, 20]); + + var json = settings.ToJson(); + var restored = UserSettings.FromJson(json); + + Assert.Equal([1, 2, 3], restored.Distribution.TargetGroupIds); + Assert.Equal([10, 20], restored.Distribution.TargetContactIds); + } + + [Fact] + public void SiteFilterDefaults_ShouldBeCorrect() + { + var filter = new SiteFilterSettings(); + + Assert.False(filter.Enabled); + Assert.Equal("none", filter.FilterType); + Assert.Empty(filter.BlockedDomains); + } +} diff --git a/TelegramMediaRelayBot.Tests/Sessions/UserSessionManagerTests.cs b/TelegramMediaRelayBot.Tests/Sessions/UserSessionManagerTests.cs new file mode 100644 index 0000000..2b7dfb7 --- /dev/null +++ b/TelegramMediaRelayBot.Tests/Sessions/UserSessionManagerTests.cs @@ -0,0 +1,86 @@ +using NSubstitute; + +namespace TelegramMediaRelayBot.Tests.Sessions; + +public class UserSessionManagerTests : IDisposable +{ + private const long TestChatId = 999_000_001; + + public UserSessionManagerTests() + { + // Clean up before each test to avoid shared state leaking + UserSessionManager.Remove(TestChatId); + UserSessionManager.Remove(TestChatId + 1); + UserSessionManager.Remove(TestChatId + 2); + } + + public void Dispose() + { + UserSessionManager.Remove(TestChatId); + UserSessionManager.Remove(TestChatId + 1); + UserSessionManager.Remove(TestChatId + 2); + } + + [Fact] + public void Set_And_Get_ShouldStoreAndRetrieveState() + { + var state = Substitute.For(); + state.GetCurrentState().Returns("TestState"); + + UserSessionManager.Set(TestChatId, state); + var retrieved = UserSessionManager.Get(TestChatId); + + Assert.NotNull(retrieved); + Assert.Equal("TestState", retrieved!.GetCurrentState()); + } + + [Fact] + public void Get_NonExistentKey_ShouldReturnNull() + { + var result = UserSessionManager.Get(TestChatId + 2); + + Assert.Null(result); + } + + [Fact] + public void ContainsKey_ShouldReturnTrueWhenExists() + { + var state = Substitute.For(); + UserSessionManager.Set(TestChatId, state); + + Assert.True(UserSessionManager.ContainsKey(TestChatId)); + } + + [Fact] + public void ContainsKey_ShouldReturnFalseWhenAbsent() + { + Assert.False(UserSessionManager.ContainsKey(TestChatId + 1)); + } + + [Fact] + public void Remove_ShouldDeleteSession() + { + var state = Substitute.For(); + UserSessionManager.Set(TestChatId, state); + + var removed = UserSessionManager.Remove(TestChatId); + + Assert.True(removed); + Assert.False(UserSessionManager.ContainsKey(TestChatId)); + Assert.Null(UserSessionManager.Get(TestChatId)); + } + + [Fact] + public void Remove_WithOutParam_ShouldReturnRemovedState() + { + var state = Substitute.For(); + state.GetCurrentState().Returns("Removable"); + UserSessionManager.Set(TestChatId, state); + + var removed = UserSessionManager.Remove(TestChatId, out var removedState); + + Assert.True(removed); + Assert.NotNull(removedState); + Assert.Equal("Removable", removedState!.GetCurrentState()); + } +} diff --git a/TelegramMediaRelayBot.Tests/TelegramMediaRelayBot.Tests.csproj b/TelegramMediaRelayBot.Tests/TelegramMediaRelayBot.Tests.csproj new file mode 100644 index 0000000..c64e37d --- /dev/null +++ b/TelegramMediaRelayBot.Tests/TelegramMediaRelayBot.Tests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + Exe + true + linux-x64 + + + + + + + + + + + + + + + + + + diff --git a/TelegramMediaRelayBot.csproj b/TelegramMediaRelayBot.csproj index d6fe337..93daed1 100644 --- a/TelegramMediaRelayBot.csproj +++ b/TelegramMediaRelayBot.csproj @@ -9,6 +9,7 @@ true true linux-x64 + $(DefaultItemExcludes);TelegramMediaRelayBot.Tests/**