344 lines
11 KiB
C#
344 lines
11 KiB
C#
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);
|