diff --git a/Crypter.Test/Assert.cs b/Crypter.Test/Assert.cs new file mode 100644 index 00000000..dd2e52e8 --- /dev/null +++ b/Crypter.Test/Assert.cs @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2026 Crypter File Transfer + * + * This file is part of the Crypter file transfer project. + * + * Crypter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Crypter source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the aforementioned license + * by purchasing a commercial license. Buying such a license is mandatory + * as soon as you develop commercial activities involving the Crypter source + * code without disclosing the source code of your own applications. + * + * Contact the current copyright holder to discuss commercial license options. + */ + +using System.Threading.Tasks; +using EasyMonads; +using NUnit.Framework; + +namespace Crypter.Test; + +/// +/// NUnit's assertions plus assertions for and . +/// +/// +/// This type shadows for every test in the Crypter.Test namespace, +/// so the inherited assertions are reached through the same Assert name they always were. +/// The monad assertions return the matched value, which lets a test assert the state and bind the +/// value in one statement. +/// +internal abstract class Assert : NUnit.Framework.Assert +{ + /// + /// Assert that the maybe is in the Some state and return the value it holds. + /// + internal static T IsSome(Maybe maybe) + { + if (maybe.IsNone) + { + throw new AssertionException($"Expected Some, but was {Describe(maybe)}."); + } + + return maybe.SomeOrDefault()!; + } + + /// + /// Assert that the maybe is in the Some state and holds , then + /// return the value it holds. + /// + internal static T IsSome(Maybe maybe, T expectedValue) + { + T value = IsSome(maybe); + That(value, Is.EqualTo(expectedValue)); + return value; + } + + /// + /// Assert that the maybe is in the None state. + /// + internal static void IsNone(Maybe maybe) + { + if (maybe.IsSome) + { + Fail($"Expected None, but was {Describe(maybe)}."); + } + } + + /// + /// Assert that the either is in the Right state and return the value it holds. + /// + internal static TRight IsRight(Either either) + { + if (!either.IsRight) + { + throw new AssertionException($"Expected Right, but was {Describe(either)}."); + } + + return either.RightOrDefault(default!)!; + } + + /// + /// Assert that the either is in the Right state and holds , then + /// return the value it holds. + /// + internal static TRight IsRight(Either either, TRight expectedValue) + { + TRight value = IsRight(either); + That(value, Is.EqualTo(expectedValue)); + return value; + } + + /// + /// Assert that the either is in the Left state and return the value it holds. + /// + internal static TLeft IsLeft(Either either) + { + if (!either.IsLeft) + { + throw new AssertionException($"Expected Left, but was {Describe(either)}."); + } + + return either.LeftOrDefault(default!)!; + } + + /// + /// Assert that the either is in the Left state and holds , then + /// return the value it holds. + /// + internal static TLeft IsLeft(Either either, TLeft expectedValue) + { + TLeft value = IsLeft(either); + That(value, Is.EqualTo(expectedValue)); + return value; + } + + /// + /// Assert that the either is in the Neither state. + /// + internal static void IsNeither(Either either) + { + if (!either.IsNeither) + { + Fail($"Expected Neither, but was {Describe(either)}."); + } + } + + /// + /// Await the task, then assert that the maybe is in the Some state and return the value it holds. + /// + internal static async Task IsSomeAsync(Task> maybeTask) + { + return IsSome(await maybeTask); + } + + /// + /// Await the task, then assert that the maybe is in the Some state and holds + /// , then return the value it holds. + /// + internal static async Task IsSomeAsync(Task> maybeTask, T expectedValue) + { + return IsSome(await maybeTask, expectedValue); + } + + /// + /// Await the task, then assert that the maybe is in the None state. + /// + internal static async Task IsNoneAsync(Task> maybeTask) + { + IsNone(await maybeTask); + } + + /// + /// Await the task, then assert that the either is in the Right state and return the value it holds. + /// + internal static async Task IsRightAsync(Task> eitherTask) + { + return IsRight(await eitherTask); + } + + /// + /// Await the task, then assert that the either is in the Right state and holds + /// , then return the value it holds. + /// + internal static async Task IsRightAsync(Task> eitherTask, TRight expectedValue) + { + return IsRight(await eitherTask, expectedValue); + } + + /// + /// Await the task, then assert that the either is in the Left state and return the value it holds. + /// + internal static async Task IsLeftAsync(Task> eitherTask) + { + return IsLeft(await eitherTask); + } + + /// + /// Await the task, then assert that the either is in the Left state and holds + /// , then return the value it holds. + /// + internal static async Task IsLeftAsync(Task> eitherTask, TLeft expectedValue) + { + return IsLeft(await eitherTask, expectedValue); + } + + /// + /// Await the task, then assert that the either is in the Neither state. + /// + internal static async Task IsNeitherAsync(Task> eitherTask) + { + IsNeither(await eitherTask); + } + + private static string Describe(Maybe maybe) + { + return maybe.IsSome + ? $"Some({maybe.SomeOrDefault()})" + : "None"; + } + + private static string Describe(Either either) + { + if (either.IsRight) + { + return $"Right({either.RightOrDefault(default!)})"; + } + + return either.IsLeft + ? $"Left({either.LeftOrDefault(default!)})" + : "Neither"; + } +} diff --git a/Crypter.Test/Common_Tests/Assert_Tests.cs b/Crypter.Test/Common_Tests/Assert_Tests.cs new file mode 100644 index 00000000..fba26ee1 --- /dev/null +++ b/Crypter.Test/Common_Tests/Assert_Tests.cs @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2026 Crypter File Transfer + * + * This file is part of the Crypter file transfer project. + * + * Crypter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Crypter source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the aforementioned license + * by purchasing a commercial license. Buying such a license is mandatory + * as soon as you develop commercial activities involving the Crypter source + * code without disclosing the source code of your own applications. + * + * Contact the current copyright holder to discuss commercial license options. + */ + +using System.Threading.Tasks; +using EasyMonads; +using NUnit.Framework; + +namespace Crypter.Test.Common_Tests; + +/// +/// Every monad assertion is exercised twice: once against the state it expects, and once against a +/// state it must reject. A helper that never fails would silently pass every test that depends on it. +/// +/// +/// The rejection tests wrap the assertion in Assert.Throws rather than a try/catch. NUnit +/// records a failed assertion against the running test as soon as it happens, and only +/// Assert.Throws clears that record. A caught still fails the +/// test. +/// +[TestFixture] +internal class Assert_Tests +{ + private const string SomeValue = "Frodo"; + private const string OtherValue = "Bilbo"; + private const int LeftValue = 42; + private const int OtherLeftValue = 24; + + private static Maybe Some => Maybe.From(SomeValue); + private static Maybe None => Maybe.None; + + private static Either Right => Either.FromRight(SomeValue); + private static Either Left => Either.FromLeft(LeftValue); + private static Either Neither => Either.Neither; + + [Test] + public void Is_Some_Returns_The_Value() + { + string value = Assert.IsSome(Some); + Assert.That(value, Is.EqualTo(SomeValue)); + } + + [Test] + public void Is_Some_Fails_For_None() + { + AssertionException? exception = Assert.Throws(() => Assert.IsSome(None)); + Assert.That(exception!.Message, Is.EqualTo("Expected Some, but was None.")); + } + + [Test] + public void Is_Some_With_An_Expected_Value_Returns_The_Value() + { + string value = Assert.IsSome(Some, SomeValue); + Assert.That(value, Is.EqualTo(SomeValue)); + } + + [Test] + public void Is_Some_With_An_Expected_Value_Fails_For_A_Different_Value() + { + Assert.Throws(() => Assert.IsSome(Some, OtherValue)); + } + + [Test] + public void Is_None_Passes_For_None() + { + Assert.IsNone(None); + } + + [Test] + public void Is_None_Fails_For_Some() + { + AssertionException? exception = Assert.Throws(() => Assert.IsNone(Some)); + Assert.That(exception!.Message, Is.EqualTo($"Expected None, but was Some({SomeValue}).")); + } + + [Test] + public void Is_Right_Returns_The_Value() + { + string value = Assert.IsRight(Right); + Assert.That(value, Is.EqualTo(SomeValue)); + } + + [Test] + public void Is_Right_Fails_For_Left() + { + AssertionException? exception = Assert.Throws(() => Assert.IsRight(Left)); + Assert.That(exception!.Message, Is.EqualTo($"Expected Right, but was Left({LeftValue}).")); + } + + [Test] + public void Is_Right_Fails_For_Neither() + { + AssertionException? exception = Assert.Throws(() => Assert.IsRight(Neither)); + Assert.That(exception!.Message, Is.EqualTo("Expected Right, but was Neither.")); + } + + [Test] + public void Is_Right_With_An_Expected_Value_Returns_The_Value() + { + string value = Assert.IsRight(Right, SomeValue); + Assert.That(value, Is.EqualTo(SomeValue)); + } + + [Test] + public void Is_Right_With_An_Expected_Value_Fails_For_A_Different_Value() + { + Assert.Throws(() => Assert.IsRight(Right, OtherValue)); + } + + [Test] + public void Is_Left_Returns_The_Value() + { + int value = Assert.IsLeft(Left); + Assert.That(value, Is.EqualTo(LeftValue)); + } + + [Test] + public void Is_Left_Fails_For_Right() + { + AssertionException? exception = Assert.Throws(() => Assert.IsLeft(Right)); + Assert.That(exception!.Message, Is.EqualTo($"Expected Left, but was Right({SomeValue}).")); + } + + [Test] + public void Is_Left_Fails_For_Neither() + { + AssertionException? exception = Assert.Throws(() => Assert.IsLeft(Neither)); + Assert.That(exception!.Message, Is.EqualTo("Expected Left, but was Neither.")); + } + + [Test] + public void Is_Left_With_An_Expected_Value_Returns_The_Value() + { + int value = Assert.IsLeft(Left, LeftValue); + Assert.That(value, Is.EqualTo(LeftValue)); + } + + [Test] + public void Is_Left_With_An_Expected_Value_Fails_For_A_Different_Value() + { + Assert.Throws(() => Assert.IsLeft(Left, OtherLeftValue)); + } + + [Test] + public void Is_Neither_Passes_For_Neither() + { + Assert.IsNeither(Neither); + } + + [Test] + public void Is_Neither_Fails_For_Right() + { + AssertionException? exception = Assert.Throws(() => Assert.IsNeither(Right)); + Assert.That(exception!.Message, Is.EqualTo($"Expected Neither, but was Right({SomeValue}).")); + } + + [Test] + public void Is_Some_Inside_A_Multiple_Scope_Does_Not_Return_A_Value() + { + bool returned = false; + + Assert.Throws(() => Assert.Multiple(() => + { + _ = Assert.IsSome(None); + returned = true; + })); + + Assert.That(returned, Is.False); + } + + [Test] + public void Is_Right_Inside_A_Multiple_Scope_Does_Not_Return_A_Value() + { + bool returned = false; + + Assert.Throws(() => Assert.Multiple(() => + { + _ = Assert.IsRight(Left); + returned = true; + })); + + Assert.That(returned, Is.False); + } + + [Test] + public void Is_Left_Inside_A_Multiple_Scope_Does_Not_Return_A_Value() + { + bool returned = false; + + Assert.Throws(() => Assert.Multiple(() => + { + _ = Assert.IsLeft(Right); + returned = true; + })); + + Assert.That(returned, Is.False); + } + + [Test] + public async Task Is_Some_Async_Returns_The_Value_Async() + { + string value = await Assert.IsSomeAsync(Task.FromResult(Some)); + Assert.That(value, Is.EqualTo(SomeValue)); + } + + [Test] + public void Is_Some_Async_Fails_For_None() + { + AssertionException? exception = + Assert.ThrowsAsync(() => Assert.IsSomeAsync(Task.FromResult(None))); + Assert.That(exception!.Message, Is.EqualTo("Expected Some, but was None.")); + } + + [Test] + public async Task Is_Some_Async_With_An_Expected_Value_Returns_The_Value_Async() + { + string value = await Assert.IsSomeAsync(Task.FromResult(Some), SomeValue); + Assert.That(value, Is.EqualTo(SomeValue)); + } + + [Test] + public void Is_Some_Async_With_An_Expected_Value_Fails_For_A_Different_Value() + { + Assert.ThrowsAsync(() => Assert.IsSomeAsync(Task.FromResult(Some), OtherValue)); + } + + [Test] + public async Task Is_None_Async_Passes_For_None_Async() + { + await Assert.IsNoneAsync(Task.FromResult(None)); + } + + [Test] + public void Is_None_Async_Fails_For_Some() + { + AssertionException? exception = + Assert.ThrowsAsync(() => Assert.IsNoneAsync(Task.FromResult(Some))); + Assert.That(exception!.Message, Is.EqualTo($"Expected None, but was Some({SomeValue}).")); + } + + [Test] + public async Task Is_Right_Async_Returns_The_Value_Async() + { + string value = await Assert.IsRightAsync(Task.FromResult(Right)); + Assert.That(value, Is.EqualTo(SomeValue)); + } + + [Test] + public void Is_Right_Async_Fails_For_Left() + { + AssertionException? exception = + Assert.ThrowsAsync(() => Assert.IsRightAsync(Task.FromResult(Left))); + Assert.That(exception!.Message, Is.EqualTo($"Expected Right, but was Left({LeftValue}).")); + } + + [Test] + public async Task Is_Right_Async_With_An_Expected_Value_Returns_The_Value_Async() + { + string value = await Assert.IsRightAsync(Task.FromResult(Right), SomeValue); + Assert.That(value, Is.EqualTo(SomeValue)); + } + + [Test] + public void Is_Right_Async_With_An_Expected_Value_Fails_For_A_Different_Value() + { + Assert.ThrowsAsync(() => Assert.IsRightAsync(Task.FromResult(Right), OtherValue)); + } + + [Test] + public async Task Is_Left_Async_Returns_The_Value_Async() + { + int value = await Assert.IsLeftAsync(Task.FromResult(Left)); + Assert.That(value, Is.EqualTo(LeftValue)); + } + + [Test] + public void Is_Left_Async_Fails_For_Right() + { + AssertionException? exception = + Assert.ThrowsAsync(() => Assert.IsLeftAsync(Task.FromResult(Right))); + Assert.That(exception!.Message, Is.EqualTo($"Expected Left, but was Right({SomeValue}).")); + } + + [Test] + public async Task Is_Left_Async_With_An_Expected_Value_Returns_The_Value_Async() + { + int value = await Assert.IsLeftAsync(Task.FromResult(Left), LeftValue); + Assert.That(value, Is.EqualTo(LeftValue)); + } + + [Test] + public void Is_Left_Async_With_An_Expected_Value_Fails_For_A_Different_Value() + { + Assert.ThrowsAsync(() => Assert.IsLeftAsync(Task.FromResult(Left), OtherLeftValue)); + } + + [Test] + public async Task Is_Neither_Async_Passes_For_Neither_Async() + { + await Assert.IsNeitherAsync(Task.FromResult(Neither)); + } + + [Test] + public void Is_Neither_Async_Fails_For_Left() + { + AssertionException? exception = + Assert.ThrowsAsync(() => Assert.IsNeitherAsync(Task.FromResult(Left))); + Assert.That(exception!.Message, Is.EqualTo($"Expected Neither, but was Left({LeftValue}).")); + } +}