Initial Enova365 sync service
This commit is contained in:
343
tests/SmartB2B.Enova.Tests/Program.cs
Normal file
343
tests/SmartB2B.Enova.Tests/Program.cs
Normal file
@@ -0,0 +1,343 @@
|
||||
using SmartB2B.Enova.Contracts;
|
||||
using SmartB2B.Enova.Service.Orders;
|
||||
using SmartB2B.Enova.Service.Rpc;
|
||||
using SmartB2B.Enova.Service.Runtime;
|
||||
using WampSharp.Core.Serialization;
|
||||
using WampSharp.V2.Core;
|
||||
using WampSharp.V2.Core.Contracts;
|
||||
using WampSharp.V2.Rpc;
|
||||
|
||||
var tests = new (string Name, Func<Task> Run)[]
|
||||
{
|
||||
("Poprawne zamówienie z rabatem ułamkowym", ValidFractionalDiscount),
|
||||
("Rabat 100 procent jest dozwolony", FullDiscount),
|
||||
("Rabat ujemny jest odrzucany", NegativeDiscount),
|
||||
("Rabat powyżej 100 jest odrzucany", DiscountAboveOneHundred),
|
||||
("Ilość zerowa jest odrzucana", ZeroQuantity),
|
||||
("Cena ujemna jest odrzucana", NegativePrice),
|
||||
("Brak towaru jest odrzucany", MissingProduct),
|
||||
("Brak pozycji jest odrzucany", MissingItems),
|
||||
("Powtarzająca się referencja nie jest walidowana", ReferenceMayRepeat),
|
||||
("Kontrakt place_order jest mapowany na Enovę", PlaceOrderMapping),
|
||||
("place_order zwraca nazwany wynik", PlaceOrderKeywordResult),
|
||||
("Warstwa WAMP wysyła wynik jako kwargs", WampKeywordWireResult),
|
||||
("Brak waluty zwraca stabilny błąd", MissingCurrency),
|
||||
("Waluta inna niż PLN jest odrzucana", UnsupportedCurrency),
|
||||
("Brak katalogu Enovy jest wykrywany", MissingEnovaDirectory)
|
||||
};
|
||||
|
||||
var failures = 0;
|
||||
foreach (var test in tests)
|
||||
{
|
||||
try
|
||||
{
|
||||
await test.Run();
|
||||
Console.WriteLine($"PASS: {test.Name}");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
failures++;
|
||||
Console.Error.WriteLine($"FAIL: {test.Name}: {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"Wynik: {tests.Length - failures}/{tests.Length} testów zaliczonych.");
|
||||
return failures == 0 ? 0 : 1;
|
||||
|
||||
static Task ValidFractionalDiscount()
|
||||
{
|
||||
AssertNoErrors(CreateValidRequest(12.5m));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task FullDiscount()
|
||||
{
|
||||
AssertNoErrors(CreateValidRequest(100m));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task NegativeDiscount()
|
||||
{
|
||||
AssertHasError(CreateValidRequest(-0.01m), "discount");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task DiscountAboveOneHundred()
|
||||
{
|
||||
AssertHasError(CreateValidRequest(100.01m), "discount");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task ZeroQuantity()
|
||||
{
|
||||
AssertHasError(CreateValidRequest(quantity: 0m), "quantity");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task NegativePrice()
|
||||
{
|
||||
AssertHasError(CreateValidRequest(unitPrice: -0.01m), "price_netto");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task MissingProduct()
|
||||
{
|
||||
AssertHasError(CreateValidRequest(productCode: ""), "symbol");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task MissingItems()
|
||||
{
|
||||
var request = new OrderRequest { CustomerCode = "Abc", Items = [] };
|
||||
AssertHasError(request, "co najmniej jedną");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task ReferenceMayRepeat()
|
||||
{
|
||||
AssertNoErrors(CreateValidRequest());
|
||||
AssertNoErrors(CreateValidRequest());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static async Task PlaceOrderMapping()
|
||||
{
|
||||
var adapter = new FakeAdapter();
|
||||
var handler = new PlaceOrderHandler(adapter, CreateConfiguration());
|
||||
var invocation = CreateInvocation();
|
||||
|
||||
await handler.HandleAsync(invocation, CancellationToken.None);
|
||||
|
||||
var request = adapter.LastRequest ?? throw new InvalidOperationException("Adapter nie otrzymał zamówienia.");
|
||||
AssertEqual("ABC", request.CustomerCode, "companyErpId");
|
||||
AssertEqual("MAG", request.WarehouseCode, "warehouseErpId");
|
||||
AssertEqual("PO/1", request.CustomerReference, "purchase_order_number");
|
||||
AssertEqual(2, request.Items.Count, "liczba pozycji");
|
||||
AssertEqual(12.5m, request.Items[0].Discount, "rabat");
|
||||
AssertEqual(120m, request.Items[0].UnitPrice, "cena netto");
|
||||
}
|
||||
|
||||
static async Task PlaceOrderKeywordResult()
|
||||
{
|
||||
var handler = new PlaceOrderHandler(new FakeAdapter(), CreateConfiguration());
|
||||
var result = await handler.HandleAsync(CreateInvocation(), CancellationToken.None);
|
||||
var keywords = result as KeywordResult
|
||||
?? throw new InvalidOperationException("Wynik nie jest wynikiem nazwanym WAMP.");
|
||||
|
||||
AssertEqual("ZO/1", keywords.Values["order_erp_symbol"], "order_erp_symbol");
|
||||
AssertEqual(100m, keywords.Values["value_netto"], "value_netto");
|
||||
AssertEqual(123m, keywords.Values["value_brutto"], "value_brutto");
|
||||
}
|
||||
|
||||
static async Task WampKeywordWireResult()
|
||||
{
|
||||
var operation = new DelegateRpcOperation(
|
||||
"test.keyword",
|
||||
(_, _) => Task.FromResult<object?>(new KeywordResult(
|
||||
new Dictionary<string, object?> { ["answer"] = 42 })));
|
||||
var callback = new FakeRouterCallback();
|
||||
|
||||
_ = operation.Invoke(
|
||||
callback,
|
||||
WampObjectFormatter.Value,
|
||||
new InvocationDetails(),
|
||||
Array.Empty<object>(),
|
||||
new Dictionary<string, object>());
|
||||
|
||||
var result = await callback.Completion.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
AssertEqual(0, result.Arguments.Length, "liczba argumentów pozycyjnych WAMP");
|
||||
AssertEqual(42, result.KeywordArguments["answer"], "argument nazwany WAMP");
|
||||
}
|
||||
|
||||
static async Task MissingCurrency()
|
||||
{
|
||||
var handler = new PlaceOrderHandler(new FakeAdapter(), CreateConfiguration());
|
||||
var invocation = new RpcInvocation([], new Dictionary<string, object?>());
|
||||
await AssertWampError(handler.HandleAsync(invocation, CancellationToken.None), "eu.smartb2b.missing_currency");
|
||||
}
|
||||
|
||||
static async Task UnsupportedCurrency()
|
||||
{
|
||||
var handler = new PlaceOrderHandler(new FakeAdapter(), CreateConfiguration());
|
||||
var invocation = new RpcInvocation([], new Dictionary<string, object?> { ["currency_iso"] = "EUR" });
|
||||
await AssertWampError(
|
||||
handler.HandleAsync(invocation, CancellationToken.None),
|
||||
"eu.smartb2b.erp.currency_not_supported");
|
||||
}
|
||||
|
||||
static Task MissingEnovaDirectory()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
_ = new EnovaAdapterLoader(path, Path.Combine(path, "adapter.dll"));
|
||||
throw new InvalidOperationException("Oczekiwano DirectoryNotFoundException.");
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
static RpcInvocation CreateInvocation() => new(
|
||||
[],
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["currency_iso"] = "PLN",
|
||||
["companyErpId"] = "ABC",
|
||||
["warehouseErpId"] = "MAG",
|
||||
["purchase_order_number"] = "PO/1",
|
||||
["notes"] = "Test",
|
||||
["lines"] = new[]
|
||||
{
|
||||
new PlaceOrderLine { Symbol = "T1", Quantity = 2m, PriceNetto = 120m, Discount = 12.5m },
|
||||
new PlaceOrderLine { Symbol = "T2", Quantity = 1m, PriceNetto = 50m, Discount = 0m }
|
||||
}
|
||||
});
|
||||
|
||||
static EnovaAdapterConfiguration CreateConfiguration() => new(
|
||||
new EnovaConnectionOptions("Firma demo", "Administrator", ""),
|
||||
"ZO",
|
||||
null,
|
||||
OrderSaveMode.Buffer);
|
||||
|
||||
static OrderRequest CreateValidRequest(
|
||||
decimal discount = 0m,
|
||||
decimal quantity = 1m,
|
||||
decimal unitPrice = 100m,
|
||||
string productCode = "BIKINI") =>
|
||||
new()
|
||||
{
|
||||
CustomerCode = "Abc",
|
||||
CustomerReference = "KLIENT/TEST/1",
|
||||
Notes = "Test",
|
||||
DocumentDefinition = "ZO",
|
||||
SaveMode = OrderSaveMode.Buffer,
|
||||
Items =
|
||||
[
|
||||
new OrderLineRequest
|
||||
{
|
||||
ProductCode = productCode,
|
||||
Quantity = quantity,
|
||||
UnitPrice = unitPrice,
|
||||
Discount = discount
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
static void AssertNoErrors(OrderRequest request)
|
||||
{
|
||||
var errors = OrderValidator.Validate(request);
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException(string.Join(" | ", errors));
|
||||
}
|
||||
}
|
||||
|
||||
static void AssertHasError(OrderRequest request, string expectedText)
|
||||
{
|
||||
var errors = OrderValidator.Validate(request);
|
||||
if (!errors.Any(error => error.Contains(expectedText, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Oczekiwano błędu zawierającego '{expectedText}', otrzymano: {string.Join(" | ", errors)}");
|
||||
}
|
||||
}
|
||||
|
||||
static async Task AssertWampError(Task<object?> task, string expectedUri)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
throw new InvalidOperationException($"Oczekiwano błędu {expectedUri}.");
|
||||
}
|
||||
catch (WampException exception) when (exception.ErrorUri == expectedUri)
|
||||
{
|
||||
// Oczekiwany błąd.
|
||||
}
|
||||
}
|
||||
|
||||
static void AssertEqual(object? expected, object? actual, string name)
|
||||
{
|
||||
if (!Equals(expected, actual))
|
||||
{
|
||||
throw new InvalidOperationException($"{name}: oczekiwano '{expected}', otrzymano '{actual}'.");
|
||||
}
|
||||
}
|
||||
|
||||
file sealed class FakeAdapter : IEnovaOrderAdapter
|
||||
{
|
||||
public OrderRequest? LastRequest { get; private set; }
|
||||
|
||||
public EnovaAdapterInfo Initialize(EnovaAdapterConfiguration configuration) =>
|
||||
new("1.0.0", "2604.4.4.0", "2604.4.4.0", configuration.Connection.DatabaseName);
|
||||
|
||||
public OrderResult CreateOrder(OrderRequest request)
|
||||
{
|
||||
LastRequest = request;
|
||||
return new OrderResult(
|
||||
1,
|
||||
"ZO/1",
|
||||
request.SaveMode,
|
||||
request.CustomerCode,
|
||||
request.WarehouseCode ?? "FIRMA",
|
||||
request.CustomerReference,
|
||||
request.Items.Count,
|
||||
100m,
|
||||
23m,
|
||||
123m,
|
||||
request.Items.Any(line => line.Discount > 0));
|
||||
}
|
||||
}
|
||||
|
||||
file sealed class FakeRouterCallback : IWampRawRpcOperationRouterCallback
|
||||
{
|
||||
public long RequestId => 1;
|
||||
|
||||
public TaskCompletionSource<CapturedResult> Completion { get; } =
|
||||
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public void Result<TMessage>(IWampFormatter<TMessage> formatter, YieldOptions details) =>
|
||||
Completion.TrySetResult(new CapturedResult([], new Dictionary<string, object?>()));
|
||||
|
||||
public void Result<TMessage>(
|
||||
IWampFormatter<TMessage> formatter,
|
||||
YieldOptions details,
|
||||
TMessage[] arguments) =>
|
||||
Completion.TrySetResult(new CapturedResult(
|
||||
arguments.Select(formatter.Deserialize<object>).ToArray(),
|
||||
new Dictionary<string, object?>()));
|
||||
|
||||
public void Result<TMessage>(
|
||||
IWampFormatter<TMessage> formatter,
|
||||
YieldOptions details,
|
||||
TMessage[] arguments,
|
||||
IDictionary<string, TMessage> argumentsKeywords) =>
|
||||
Completion.TrySetResult(new CapturedResult(
|
||||
arguments.Select(formatter.Deserialize<object>).ToArray(),
|
||||
argumentsKeywords.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => (object?)formatter.Deserialize<object>(pair.Value))));
|
||||
|
||||
public void Error<TMessage>(IWampFormatter<TMessage> formatter, TMessage details, string error) =>
|
||||
Completion.TrySetException(new InvalidOperationException(error));
|
||||
|
||||
public void Error<TMessage>(
|
||||
IWampFormatter<TMessage> formatter,
|
||||
TMessage details,
|
||||
string error,
|
||||
TMessage[] arguments) =>
|
||||
Completion.TrySetException(new InvalidOperationException(error));
|
||||
|
||||
public void Error<TMessage>(
|
||||
IWampFormatter<TMessage> formatter,
|
||||
TMessage details,
|
||||
string error,
|
||||
TMessage[] arguments,
|
||||
TMessage argumentsKeywords) =>
|
||||
Completion.TrySetException(new InvalidOperationException(error));
|
||||
}
|
||||
|
||||
file sealed record CapturedResult(
|
||||
object?[] Arguments,
|
||||
IReadOnlyDictionary<string, object?> KeywordArguments);
|
||||
12
tests/SmartB2B.Enova.Tests/SmartB2B.Enova.Tests.csproj
Normal file
12
tests/SmartB2B.Enova.Tests/SmartB2B.Enova.Tests.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/SmartB2B.Enova.Contracts/SmartB2B.Enova.Contracts.csproj" />
|
||||
<ProjectReference Include="../../src/SmartB2B.Enova.Service/SmartB2B.Enova.Service.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user