Initial Commit after switching from SVN to git
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using programmersdigest.MT940Parser;
|
||||
using programmersdigest.MT940Parser.Parsing;
|
||||
using Xunit;
|
||||
|
||||
namespace Fuchs.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// MT940 BalanceParser robustness tests.
|
||||
/// </summary>
|
||||
public class BalanceParserTests
|
||||
{
|
||||
private readonly BalanceParser _parser = new();
|
||||
|
||||
[Fact]
|
||||
public void ReadBalance_ValidCredit_ReturnsCorrectBalance()
|
||||
{
|
||||
// C230101EUR1000,00
|
||||
var balance = _parser.ReadBalance("C230101EUR1000,00", BalanceType.Opening);
|
||||
Assert.Equal(DebitCreditMark.Credit, balance.Mark);
|
||||
Assert.Equal(new DateTime(2023, 1, 1), balance.Date);
|
||||
Assert.Equal("EUR", balance.Currency);
|
||||
Assert.Equal(1000.00m, balance.Amount);
|
||||
Assert.Equal(BalanceType.Opening, balance.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBalance_ValidDebit_SetsDebitMark()
|
||||
{
|
||||
var balance = _parser.ReadBalance("D230601USD500,50", BalanceType.Closing);
|
||||
Assert.Equal(DebitCreditMark.Debit, balance.Mark);
|
||||
Assert.Equal("USD", balance.Currency);
|
||||
Assert.Equal(500.50m, balance.Amount);
|
||||
Assert.Equal(BalanceType.Closing, balance.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBalance_InvalidMark_ThrowsFormatException()
|
||||
{
|
||||
Assert.Throws<FormatException>(() => _parser.ReadBalance("X230101EUR1000,00", BalanceType.Opening));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBalance_EmptyString_ThrowsInvalidDataException()
|
||||
{
|
||||
Assert.Throws<InvalidDataException>(() => _parser.ReadBalance("", BalanceType.Opening));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBalance_TruncatedAfterMark_ThrowsInvalidDataException()
|
||||
{
|
||||
// Only "C" — no date follows
|
||||
Assert.Throws<InvalidDataException>(() => _parser.ReadBalance("C", BalanceType.Opening));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBalance_TruncatedAfterDate_ThrowsInvalidDataException()
|
||||
{
|
||||
// "C230101" — no currency
|
||||
Assert.Throws<InvalidDataException>(() => _parser.ReadBalance("C230101", BalanceType.Opening));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBalance_TruncatedAfterCurrency_ThrowsInvalidDataException()
|
||||
{
|
||||
// "C230101EUR" — no amount
|
||||
Assert.Throws<InvalidDataException>(() => _parser.ReadBalance("C230101EUR", BalanceType.Opening));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBalance_InvalidAmount_ThrowsFormatException()
|
||||
{
|
||||
Assert.Throws<FormatException>(() => _parser.ReadBalance("C230101EURabc", BalanceType.Opening));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBalance_LowercaseCurrency_ThrowsInvalidDataException()
|
||||
{
|
||||
Assert.Throws<InvalidDataException>(() => _parser.ReadBalance("C230101eur1000,00", BalanceType.Opening));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBalance_ZeroAmount_ParsesCorrectly()
|
||||
{
|
||||
var balance = _parser.ReadBalance("C230101EUR0,00", BalanceType.Opening);
|
||||
Assert.Equal(0m, balance.Amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Fuchs.intranet;
|
||||
using programmersdigest.MT940Parser;
|
||||
using Xunit;
|
||||
|
||||
namespace Fuchs.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Banking helper robustness tests.
|
||||
/// </summary>
|
||||
public class BankingDebitCreditMarkTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(DebitCreditMark.Credit, "C")]
|
||||
[InlineData(DebitCreditMark.Debit, "D")]
|
||||
[InlineData(DebitCreditMark.ReverseCredit, "RC")]
|
||||
[InlineData(DebitCreditMark.ReverseDebit, "RD")]
|
||||
public void DebitCreditMarkAbb_ReturnsExpected(DebitCreditMark mark, string expected)
|
||||
{
|
||||
Assert.Equal(expected, Banking.DebitCreditMarkAbb(mark));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DebitCreditMarkAbb_UndefinedValue_ReturnsEmpty()
|
||||
{
|
||||
Assert.Equal("", Banking.DebitCreditMarkAbb((DebitCreditMark)999));
|
||||
}
|
||||
}
|
||||
|
||||
public class BankingParseToDatatableTests
|
||||
{
|
||||
private static readonly string MinimalMT940 =
|
||||
"\r\n:20:STARTUMSE\r\n" +
|
||||
":25:DE12345678901234567890\r\n" +
|
||||
":28C:00001/001\r\n" +
|
||||
":60F:C230101EUR1000,00\r\n" +
|
||||
":61:2301010101CR500,00NTRFREF123\r\n" +
|
||||
":86:Gutschrift\r\n" +
|
||||
":62F:C230101EUR1500,00\r\n" +
|
||||
"-\r\n";
|
||||
|
||||
private static Stream ToStream(string content)
|
||||
=> new MemoryStream(Encoding.UTF8.GetBytes(content));
|
||||
|
||||
[Fact]
|
||||
public void ParseToDatatable_ValidMT940_ReturnsOneRow()
|
||||
{
|
||||
using var stream = ToStream(MinimalMT940);
|
||||
var table = Banking.ParseToDatatable(stream);
|
||||
Assert.Equal(1, table.Rows.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseToDatatable_ValidMT940_HasAccountColumn()
|
||||
{
|
||||
using var stream = ToStream(MinimalMT940);
|
||||
var table = Banking.ParseToDatatable(stream);
|
||||
Assert.True(table.Columns.Contains("AccountIdentification"));
|
||||
Assert.Equal("DE12345678901234567890", table.Rows[0]["AccountIdentification"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseToDatatable_ValidMT940_HasAmountColumn()
|
||||
{
|
||||
using var stream = ToStream(MinimalMT940);
|
||||
var table = Banking.ParseToDatatable(stream);
|
||||
Assert.True(table.Columns.Contains("Amount"));
|
||||
Assert.Equal(500m, table.Rows[0]["Amount"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseToDatatable_ValidMT940_HasDebitCreditMark()
|
||||
{
|
||||
using var stream = ToStream(MinimalMT940);
|
||||
var table = Banking.ParseToDatatable(stream);
|
||||
Assert.Equal("C", table.Rows[0]["DebitCreditMark"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseToDatatable_EmptyStream_ReturnsEmptyTable()
|
||||
{
|
||||
using var stream = ToStream("");
|
||||
var table = Banking.ParseToDatatable(stream);
|
||||
Assert.Equal(0, table.Rows.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseToDatatable_EmptyStream_HasDefaultSchema()
|
||||
{
|
||||
using var stream = ToStream("");
|
||||
var table = Banking.ParseToDatatable(stream);
|
||||
Assert.True(table.Columns.Contains("AccountIdentification"));
|
||||
Assert.True(table.Columns.Contains("Amount"));
|
||||
Assert.True(table.Columns.Contains("DebitCreditMark"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseToDatatable_WithCustomSchema_UsesProvidedSchema()
|
||||
{
|
||||
var schema = new DataTable();
|
||||
schema.Columns.Add("AccountIdentification", typeof(string));
|
||||
schema.Columns.Add("Amount", typeof(decimal));
|
||||
|
||||
using var stream = ToStream(MinimalMT940);
|
||||
var table = Banking.ParseToDatatable(stream, schemaDatatable: schema);
|
||||
Assert.Equal(2, table.Columns.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseToDatatable_MultipleStatements_ParsesAll()
|
||||
{
|
||||
var multi = MinimalMT940 + "\n" + MinimalMT940;
|
||||
using var stream = ToStream(multi);
|
||||
var table = Banking.ParseToDatatable(stream);
|
||||
Assert.Equal(2, table.Rows.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseToDatatable_MalformedContent_DoesNotThrow()
|
||||
{
|
||||
using var stream = ToStream("This is not MT940 data at all");
|
||||
var ex = Record.Exception(() => Banking.ParseToDatatable(stream));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using programmersdigest.MT940Parser.Parsing;
|
||||
using Xunit;
|
||||
|
||||
namespace Fuchs.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// MT940 DateParser robustness tests.
|
||||
/// </summary>
|
||||
public class DateParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_ValidDate_ReturnsCorrectDateTime()
|
||||
{
|
||||
var result = DateParser.Parse("230115");
|
||||
Assert.Equal(new DateTime(2023, 1, 15), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Year79_Returns2079()
|
||||
{
|
||||
// 79 is not > 79, so year += 2000
|
||||
var result = DateParser.Parse("790601");
|
||||
Assert.Equal(2079, result.Year);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Year80_Returns1980()
|
||||
{
|
||||
// 80 > 79 is true, so year += 1900
|
||||
var result = DateParser.Parse("800101");
|
||||
Assert.Equal(1980, result.Year);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Year00_Returns2000()
|
||||
{
|
||||
var result = DateParser.Parse("000315");
|
||||
Assert.Equal(2000, result.Year);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_NullInput_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => DateParser.Parse(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_WrongLength_ThrowsFormatException()
|
||||
{
|
||||
Assert.Throws<FormatException>(() => DateParser.Parse("12345"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_TooLong_ThrowsFormatException()
|
||||
{
|
||||
Assert.Throws<FormatException>(() => DateParser.Parse("1234567"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_NonNumeric_ThrowsFormatException()
|
||||
{
|
||||
Assert.Throws<FormatException>(() => DateParser.Parse("23AB15"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_EmptyString_ThrowsFormatException()
|
||||
{
|
||||
Assert.Throws<FormatException>(() => DateParser.Parse(""));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("231231", 2023, 12, 31)]
|
||||
[InlineData("990101", 1999, 1, 1)]
|
||||
[InlineData("010228", 2001, 2, 28)]
|
||||
public void Parse_VariousDates_ReturnsExpected(string input, int year, int month, int day)
|
||||
{
|
||||
var result = DateParser.Parse(input);
|
||||
Assert.Equal(new DateTime(year, month, day), result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using MFR_RESTClient.generic;
|
||||
using Xunit;
|
||||
|
||||
namespace Fuchs.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// MfrGeneric EntityHelper and TryConvert robustness tests.
|
||||
/// </summary>
|
||||
public class EntityHelperTests
|
||||
{
|
||||
[Fact]
|
||||
public void EntityName_StandardType_ReturnsCorrectName()
|
||||
{
|
||||
Assert.Equal("Item", EntityHelper.EntityName(EntityTypes.Item));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntityName_ServiceRequest_ReturnsUnmodified()
|
||||
{
|
||||
Assert.Equal("ServiceRequest", EntityHelper.EntityName(EntityTypes.ServiceRequest));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntityValue_ValidName_ReturnsEnum()
|
||||
{
|
||||
Assert.Equal(EntityTypes.Invoice, EntityHelper.EntityValue("Invoice"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntityValue_CaseInsensitive_ReturnsEnum()
|
||||
{
|
||||
Assert.Equal(EntityTypes.Invoice, EntityHelper.EntityValue("invoice"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntityValue_UnknownName_ReturnsNone()
|
||||
{
|
||||
Assert.Equal(EntityTypes.none, EntityHelper.EntityValue("NonExistentEntity"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntityValue_EmptyString_ReturnsNone()
|
||||
{
|
||||
Assert.Equal(EntityTypes.none, EntityHelper.EntityValue(""));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(EntityTypes.Item)]
|
||||
[InlineData(EntityTypes.Invoice)]
|
||||
[InlineData(EntityTypes.CostCenter)]
|
||||
[InlineData(EntityTypes.ServiceObject)]
|
||||
[InlineData(EntityTypes.StepListTemplate)]
|
||||
public void EntityName_EntityValue_RoundTrip(EntityTypes et)
|
||||
{
|
||||
var name = EntityHelper.EntityName(et);
|
||||
var result = EntityHelper.EntityValue(name);
|
||||
Assert.Equal(et, result);
|
||||
}
|
||||
}
|
||||
|
||||
public class TryConvertTests
|
||||
{
|
||||
[Fact]
|
||||
public void TryConvert_StringToInt_ReturnsCorrectValue()
|
||||
{
|
||||
var result = "42".TryConvert(typeof(int));
|
||||
Assert.Equal(42, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryConvert_StringToDouble_ReturnsCorrectValue()
|
||||
{
|
||||
var result = "3.14".TryConvert(typeof(double));
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(3.14, (double)result!, precision: 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryConvert_StringToBool_ReturnsTrue()
|
||||
{
|
||||
var result = "True".TryConvert(typeof(bool));
|
||||
Assert.Equal(true, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryConvert_InvalidString_ThrowsArgumentException()
|
||||
{
|
||||
// TypeDescriptor.GetConverter throws ArgumentException for invalid int, not caught by TryConvert
|
||||
Assert.ThrowsAny<Exception>(() => "not-a-number".TryConvert(typeof(int)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryConvert_EmptyString_ToInt_ThrowsException()
|
||||
{
|
||||
Assert.ThrowsAny<Exception>(() => "".TryConvert(typeof(int)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryConvert_StringToDateTime_ReturnsValue()
|
||||
{
|
||||
var result = "2023-01-15".TryConvert(typeof(DateTime));
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<DateTime>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryConvert_StringToDecimal_ReturnsValue()
|
||||
{
|
||||
var result = "99.99".TryConvert(typeof(decimal));
|
||||
Assert.Equal(99.99m, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Fuchs\Fuchs.csproj" />
|
||||
<ProjectReference Include="..\Fuchs_DataService\Fuchs_DataService.csproj" />
|
||||
<ProjectReference Include="..\MFR_RESTClient\MFR_RESTClient.csproj" />
|
||||
<ProjectReference Include="..\..\..\WebProjectComponents\MT940Parser\MT940Parser\MT940Parser.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,64 @@
|
||||
using Fuchs.intranet;
|
||||
using Xunit;
|
||||
|
||||
namespace Fuchs.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Priority 4: Web application PDF helpers and data model logic.
|
||||
/// </summary>
|
||||
public class FuchsPdfParseDecTests
|
||||
{
|
||||
// ParseDec uses InvariantCulture: period is decimal separator, comma is group separator
|
||||
[Theory]
|
||||
[InlineData("1000.50", 1000.50)]
|
||||
[InlineData("0", 0)]
|
||||
[InlineData("123", 123)]
|
||||
[InlineData("-99.99", -99.99)]
|
||||
public void ParseDec_ValidInput_ReturnsTrueAndCorrectValue(string input, decimal expected)
|
||||
{
|
||||
var result = FuchsPdf.ParseDec(input, out decimal val);
|
||||
Assert.True(result);
|
||||
Assert.Equal(expected, val);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseDec_NullInput_ReturnsFalse()
|
||||
{
|
||||
var result = FuchsPdf.ParseDec(null, out decimal val);
|
||||
Assert.False(result);
|
||||
Assert.Equal(0m, val);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseDec_EmptyString_ReturnsFalse()
|
||||
{
|
||||
var result = FuchsPdf.ParseDec("", out decimal val);
|
||||
Assert.False(result);
|
||||
Assert.Equal(0m, val);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseDec_NonNumericString_ReturnsFalse()
|
||||
{
|
||||
var result = FuchsPdf.ParseDec("abc", out decimal val);
|
||||
Assert.False(result);
|
||||
Assert.Equal(0m, val);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseDec_DecimalStringInput_ReturnsTrueAndValue()
|
||||
{
|
||||
// Pass decimal as string to avoid ToString() culture dependency
|
||||
var result = FuchsPdf.ParseDec("42.5", out decimal val);
|
||||
Assert.True(result);
|
||||
Assert.Equal(42.5m, val);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseDec_IntegerInput_ReturnsTrueAndValue()
|
||||
{
|
||||
var result = FuchsPdf.ParseDec(100, out decimal val);
|
||||
Assert.True(result);
|
||||
Assert.Equal(100m, val);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using MFR_RESTClient;
|
||||
using Xunit;
|
||||
|
||||
namespace Fuchs.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// HtmlCleanUp.Clean robustness tests.
|
||||
/// </summary>
|
||||
public class HtmlCleanUpTests
|
||||
{
|
||||
[Fact]
|
||||
public void Clean_PlainText_ReturnsSameText()
|
||||
{
|
||||
Assert.Equal("Hello World", HtmlCleanUp.Clean("Hello World"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clean_HtmlTags_AreStripped()
|
||||
{
|
||||
Assert.Equal("Bold text", HtmlCleanUp.Clean("<b>Bold</b> <i>text</i>"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clean_ParagraphTags_ConvertToNewlines()
|
||||
{
|
||||
var result = HtmlCleanUp.Clean("<p>Line1</p><p>Line2</p>");
|
||||
Assert.Contains("Line1", result);
|
||||
Assert.Contains("Line2", result);
|
||||
Assert.Contains("\n", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clean_BrTags_ConvertToNewlines()
|
||||
{
|
||||
var result = HtmlCleanUp.Clean("Line1<br>Line2<br/>Line3");
|
||||
Assert.Contains("\n", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clean_DivTags_ConvertToNewlines()
|
||||
{
|
||||
var result = HtmlCleanUp.Clean("<div>Block1</div><div>Block2</div>");
|
||||
Assert.Contains("\n", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clean_HtmlEntities_AreDecoded()
|
||||
{
|
||||
Assert.Equal("A & B", HtmlCleanUp.Clean("A & B"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clean_MultipleNewlines_CollapsedToOne()
|
||||
{
|
||||
var result = HtmlCleanUp.Clean("<p></p><p></p><p>Text</p>");
|
||||
// Should not have consecutive newlines
|
||||
Assert.DoesNotContain("\n\n", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clean_LeadingTrailingNewlines_AreRemoved()
|
||||
{
|
||||
var result = HtmlCleanUp.Clean("<p>Text</p>");
|
||||
Assert.False(result.StartsWith("\n"));
|
||||
Assert.False(result.EndsWith("\n"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clean_EmptyString_ReturnsEmpty()
|
||||
{
|
||||
Assert.Equal("", HtmlCleanUp.Clean(""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clean_NestedTags_StripsAll()
|
||||
{
|
||||
Assert.Equal("deep", HtmlCleanUp.Clean("<div><span><b>deep</b></span></div>"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clean_TagsWithAttributes_AreStripped()
|
||||
{
|
||||
Assert.Equal("link", HtmlCleanUp.Clean("<a href=\"https://example.com\" class=\"btn\">link</a>"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("<script>", "<script>")]
|
||||
[InlineData(""hello"", "\"hello\"")]
|
||||
[InlineData("'test'", "'test'")]
|
||||
public void Clean_VariousEntities_AreDecoded(string input, string expected)
|
||||
{
|
||||
Assert.Equal(expected, HtmlCleanUp.Clean(input));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using MFR_RESTClient;
|
||||
using Xunit;
|
||||
|
||||
namespace Fuchs.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Priority 3: REST client model construction and serialization.
|
||||
/// </summary>
|
||||
public class MFRClientConfigTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_WithHttpUrl_PreservesScheme()
|
||||
{
|
||||
var config = new MFRClientConfig("http://example.com");
|
||||
Assert.StartsWith("http://", config.BaseUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithHttpsUrl_PreservesScheme()
|
||||
{
|
||||
var config = new MFRClientConfig("https://example.com");
|
||||
Assert.StartsWith("https://", config.BaseUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithoutScheme_PrependHttps()
|
||||
{
|
||||
var config = new MFRClientConfig("example.com");
|
||||
Assert.StartsWith("https://", config.BaseUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_BaseUrlAlwaysEndsWithSlash()
|
||||
{
|
||||
var config = new MFRClientConfig("https://example.com/odata");
|
||||
Assert.EndsWith("/", config.BaseUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithoutScheme_AppendsOdataPath()
|
||||
{
|
||||
var config = new MFRClientConfig("example.com");
|
||||
Assert.Contains("/odata/", config.BaseUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultValues_AreCorrect()
|
||||
{
|
||||
var config = new MFRClientConfig("https://example.com/");
|
||||
Assert.False(config.IsLogoutRequired);
|
||||
Assert.False(config.IsSessionRequired);
|
||||
Assert.Equal("", config.LoginAddress);
|
||||
Assert.Equal("", config.LogoutAddress);
|
||||
Assert.Equal("", config.TokenCookieName);
|
||||
}
|
||||
}
|
||||
|
||||
public class MFRClientCredentialsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsUsernameAndPassword()
|
||||
{
|
||||
var creds = new MFRClientCredentials("admin", "secret");
|
||||
Assert.Equal("admin", creds.Username);
|
||||
Assert.Equal("secret", creds.Password);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_AllowsEmptyCredentials()
|
||||
{
|
||||
var creds = new MFRClientCredentials("", "");
|
||||
Assert.Equal("", creds.Username);
|
||||
Assert.Equal("", creds.Password);
|
||||
}
|
||||
}
|
||||
|
||||
public class MFRClientExceptionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsAllProperties()
|
||||
{
|
||||
var ex = new MFRClientException(HttpStatusCode.Unauthorized, "Unauthorized", "Token expired");
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, ex.StatusCode);
|
||||
Assert.Equal("Unauthorized", ex.Error);
|
||||
Assert.Equal("Token expired", ex.Content);
|
||||
Assert.Equal("Unauthorized", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithInnerException_ChainsProperly()
|
||||
{
|
||||
var inner = new InvalidOperationException("inner");
|
||||
var ex = new MFRClientException(HttpStatusCode.InternalServerError, "Error", "body", inner);
|
||||
Assert.Same(inner, ex.InnerException);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using programmersdigest.MT940Parser;
|
||||
using Xunit;
|
||||
|
||||
namespace Fuchs.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Priority 1: MT940 statement parsing correctness.
|
||||
/// </summary>
|
||||
public class MT940ParserTests
|
||||
{
|
||||
// Minimal valid MT940 fixture — parser requires CRLF and leading \r\n before :20:
|
||||
private static readonly string MinimalMT940 =
|
||||
"\r\n:20:STARTUMSE\r\n" +
|
||||
":25:DE12345678901234567890\r\n" +
|
||||
":28C:00001/001\r\n" +
|
||||
":60F:C230101EUR1000,00\r\n" +
|
||||
":61:2301010101CR500,00NTRFREF123\r\n" +
|
||||
":86:Gutschrift\r\n" +
|
||||
":62F:C230101EUR1500,00\r\n" +
|
||||
"-\r\n";
|
||||
|
||||
private static Stream ToStream(string content)
|
||||
=> new MemoryStream(Encoding.UTF8.GetBytes(content));
|
||||
|
||||
[Fact]
|
||||
public void Parse_ValidMT940_ReturnsOneStatement()
|
||||
{
|
||||
using var parser = new Parser(ToStream(MinimalMT940));
|
||||
var statements = parser.Parse().ToList();
|
||||
Assert.Single(statements);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ValidMT940_HasCorrectAccountIdentification()
|
||||
{
|
||||
using var parser = new Parser(ToStream(MinimalMT940));
|
||||
var statement = parser.Parse().First();
|
||||
Assert.Equal("DE12345678901234567890", statement.AccountIdentification);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ValidMT940_HasCorrectTransactionReference()
|
||||
{
|
||||
using var parser = new Parser(ToStream(MinimalMT940));
|
||||
var statement = parser.Parse().First();
|
||||
Assert.Equal("STARTUMSE", statement.TransactionReferenceNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ValidMT940_HasOneStatementLine()
|
||||
{
|
||||
using var parser = new Parser(ToStream(MinimalMT940));
|
||||
var statement = parser.Parse().First();
|
||||
Assert.Single(statement.Lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ValidMT940_OpeningBalanceCreditEUR()
|
||||
{
|
||||
using var parser = new Parser(ToStream(MinimalMT940));
|
||||
var statement = parser.Parse().First();
|
||||
Assert.Equal("EUR", statement.OpeningBalance.Currency);
|
||||
Assert.Equal(DebitCreditMark.Credit, statement.OpeningBalance.Mark);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ValidMT940_ClosingBalanceAmount()
|
||||
{
|
||||
using var parser = new Parser(ToStream(MinimalMT940));
|
||||
var statement = parser.Parse().First();
|
||||
Assert.Equal(1500m, statement.ClosingBalance.Amount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_EmptyStream_ReturnsNoStatements()
|
||||
{
|
||||
using var parser = new Parser(ToStream(string.Empty));
|
||||
var statements = parser.Parse().ToList();
|
||||
Assert.Empty(statements);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_MultipleStatements_ReturnsBoth()
|
||||
{
|
||||
var multiStatement = MinimalMT940 + "\n" + MinimalMT940;
|
||||
using var parser = new Parser(ToStream(multiStatement));
|
||||
var statements = parser.Parse().ToList();
|
||||
Assert.Equal(2, statements.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_DoesNotThrow()
|
||||
{
|
||||
var parser = new Parser(ToStream(MinimalMT940));
|
||||
var exception = Record.Exception(() => parser.Dispose());
|
||||
Assert.Null(exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using MFR_RESTClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Fuchs.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// ODataEnvelope parsing robustness tests.
|
||||
/// </summary>
|
||||
public class ODataEnvelopeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_NullContent_DoesNotThrow()
|
||||
{
|
||||
var envelope = new ODataEnvelope(null);
|
||||
Assert.Null(envelope.Value);
|
||||
Assert.Null(envelope.Metadata);
|
||||
Assert.Null(envelope.NextLink);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_EmptyContent_DoesNotThrow()
|
||||
{
|
||||
var envelope = new ODataEnvelope("");
|
||||
Assert.Null(envelope.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithValueArray_ParsesValue()
|
||||
{
|
||||
var json = """{"value": [{"id": 1}, {"id": 2}]}""";
|
||||
var envelope = new ODataEnvelope(json);
|
||||
Assert.NotNull(envelope.Value);
|
||||
Assert.IsType<JArray>(envelope.Value);
|
||||
Assert.Equal(2, ((JArray)envelope.Value).Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMetadata_ParsesUri()
|
||||
{
|
||||
var json = """{"odata.metadata": "https://example.com/$metadata#Items", "value": []}""";
|
||||
var envelope = new ODataEnvelope(json);
|
||||
Assert.NotNull(envelope.Metadata);
|
||||
Assert.Equal("https://example.com/$metadata#Items", envelope.Metadata!.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNextLink_ParsesUri()
|
||||
{
|
||||
var json = """{"odata.nextLink": "https://example.com/odata/Items?$skip=50", "value": []}""";
|
||||
var envelope = new ODataEnvelope(json);
|
||||
Assert.NotNull(envelope.NextLink);
|
||||
Assert.Contains("$skip=50", envelope.NextLink!.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NonHttpMetadata_DoesNotSetMetadata()
|
||||
{
|
||||
var json = """{"odata.metadata": "not-a-url", "value": []}""";
|
||||
var envelope = new ODataEnvelope(json);
|
||||
Assert.Null(envelope.Metadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NoValueKey_DeserializesWholeObject()
|
||||
{
|
||||
var json = """{"name": "test", "count": 5}""";
|
||||
var envelope = new ODataEnvelope(json);
|
||||
Assert.NotNull(envelope.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsUrlFromParameter()
|
||||
{
|
||||
var envelope = new ODataEnvelope("{}", url: "https://example.com/odata/Items");
|
||||
Assert.Equal("https://example.com/odata/Items", envelope.Url);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertToArray_SingleObject_WrapsInJArray()
|
||||
{
|
||||
var json = """{"name": "test"}""";
|
||||
var envelope = new ODataEnvelope(json);
|
||||
envelope.ConvertToArray();
|
||||
Assert.IsType<JArray>(envelope.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertToArray_AlreadyJArray_RemainsJArray()
|
||||
{
|
||||
var json = """{"value": [{"id": 1}]}""";
|
||||
var envelope = new ODataEnvelope(json);
|
||||
envelope.ConvertToArray();
|
||||
Assert.IsType<JArray>(envelope.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertToArray_NullValue_DoesNotThrow()
|
||||
{
|
||||
var envelope = new ODataEnvelope(null);
|
||||
var ex = Record.Exception(() => envelope.ConvertToArray());
|
||||
Assert.Null(ex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using programmersdigest.MT940Parser;
|
||||
using programmersdigest.MT940Parser.Parsing;
|
||||
using Xunit;
|
||||
|
||||
namespace Fuchs.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// MT940 StatementLineParser robustness tests.
|
||||
/// </summary>
|
||||
public class StatementLineParserTests
|
||||
{
|
||||
private readonly StatementLineParser _parser = new();
|
||||
|
||||
// Minimal valid: 6-digit valueDate + mark + amount + transType(1) + typeCode(3) + customerRef
|
||||
// Example: 230101C500,00NTRFREF123
|
||||
private const string ValidLine = "230101C500,00NTRFREF123";
|
||||
|
||||
[Fact]
|
||||
public void ReadStatementLine_ValidCredit_ParsesCorrectly()
|
||||
{
|
||||
var line = _parser.ReadStatementLine(ValidLine);
|
||||
Assert.Equal(new DateTime(2023, 1, 1), line.ValueDate);
|
||||
Assert.Equal(DebitCreditMark.Credit, line.Mark);
|
||||
Assert.Equal(500m, line.Amount);
|
||||
Assert.Equal("TRF", line.TransactionTypeIdCode);
|
||||
Assert.Equal("REF123", line.CustomerReference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadStatementLine_DebitMark_SetsDebit()
|
||||
{
|
||||
var line = _parser.ReadStatementLine("230101D100,50NTRFPAY001");
|
||||
Assert.Equal(DebitCreditMark.Debit, line.Mark);
|
||||
Assert.Equal(100.50m, line.Amount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadStatementLine_ReverseCreditMark_SetsReverseCredit()
|
||||
{
|
||||
var line = _parser.ReadStatementLine("230101RC100,00NTRFREF001");
|
||||
Assert.Equal(DebitCreditMark.ReverseCredit, line.Mark);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadStatementLine_ReverseDebitMark_SetsReverseDebit()
|
||||
{
|
||||
var line = _parser.ReadStatementLine("230101RD100,00NTRFREF001");
|
||||
Assert.Equal(DebitCreditMark.ReverseDebit, line.Mark);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadStatementLine_WithEntryDate_ParsesEntryDate()
|
||||
{
|
||||
// valueDate=230620, entryDate=0615 (4 digits), mark=C, amount=500,00, type=N, code=TRF, ref=REF123
|
||||
// entryDate (June 15) <= valueDate (June 20) so no year correction
|
||||
var line = _parser.ReadStatementLine("2306200615C500,00NTRFREF123");
|
||||
Assert.Equal(new DateTime(2023, 6, 20), line.ValueDate);
|
||||
Assert.Equal(new DateTime(2023, 6, 15), line.EntryDate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadStatementLine_WithFundsCode_ParsesFundsCode()
|
||||
{
|
||||
// FundsCode is a single letter between mark and amount
|
||||
var line = _parser.ReadStatementLine("230101DA500,00NTRFREF123");
|
||||
Assert.Equal('A', line.FundsCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadStatementLine_EmptyString_ThrowsInvalidDataException()
|
||||
{
|
||||
Assert.Throws<InvalidDataException>(() => _parser.ReadStatementLine(""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadStatementLine_TruncatedValueDate_ThrowsInvalidDataException()
|
||||
{
|
||||
Assert.Throws<InvalidDataException>(() => _parser.ReadStatementLine("2301"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadStatementLine_InvalidTransactionType_ThrowsInvalidDataException()
|
||||
{
|
||||
// 'X' is not a valid transaction type (must be N, S, or F)
|
||||
Assert.Throws<InvalidDataException>(() => _parser.ReadStatementLine("230101C500,00XTRFREF123"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadStatementLine_InvalidMark_ThrowsFormatException()
|
||||
{
|
||||
Assert.Throws<FormatException>(() => _parser.ReadStatementLine("230101X500,00NTRFREF123"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadStatementLine_TransactionTypeS_Accepted()
|
||||
{
|
||||
var line = _parser.ReadStatementLine("230101C500,00STRFREF123");
|
||||
Assert.Equal("TRF", line.TransactionTypeIdCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadStatementLine_TransactionTypeF_Accepted()
|
||||
{
|
||||
var line = _parser.ReadStatementLine("230101C500,00FTRFREF123");
|
||||
Assert.Equal("TRF", line.TransactionTypeIdCode);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user