mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
Rate Policy
This commit is contained in:
@@ -9,7 +9,7 @@ using System.Threading.Tasks;
|
||||
namespace Esiur.Tests.Functional;
|
||||
|
||||
[Export]
|
||||
public class MyRecord:IRecord
|
||||
public class MyRecord : IMyRecord
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Id { get; set; }
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using Esiur.Data;
|
||||
using Esiur.Core;
|
||||
using Esiur.Data.Types;
|
||||
using Esiur.Resource;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Esiur.Protocol;
|
||||
|
||||
#nullable enable
|
||||
@@ -142,7 +143,7 @@ public partial class MyService
|
||||
[Export] public IRecord[] RecordsArray => new IRecord[] { new MyRecord() { Id = 22, Name = "Test", Score = 22.1 } };
|
||||
[Export] public List<MyRecord> RecordsList => new() { new MyRecord() { Id = 22, Name = "Test", Score = 22.1 } };
|
||||
|
||||
[Export] public IMyRecord myrecord { get; set; }
|
||||
[Export] public IMyRecord myrecord { get; set; } = new MyRecord();
|
||||
|
||||
[Export] public MyResource[]? myResources;
|
||||
|
||||
@@ -214,4 +215,92 @@ public partial class MyService
|
||||
|
||||
[Export] public static string staticFunction(string name) => $"Hello {name}";
|
||||
|
||||
int _terminatedPullStreams;
|
||||
|
||||
public int TerminatedPullStreams => Volatile.Read(ref _terminatedPullStreams);
|
||||
|
||||
[Export]
|
||||
[RateControl("standard-call")]
|
||||
public int RateLimitedCall() => 1;
|
||||
|
||||
[Export]
|
||||
[RateControl("standard-set")]
|
||||
public int RateLimitedValue { get; set; }
|
||||
|
||||
[Export]
|
||||
[Cancellable]
|
||||
public async IAsyncEnumerable<int> PullRange(
|
||||
int start,
|
||||
int count,
|
||||
int delayMilliseconds,
|
||||
InvocationContext context)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
await Task.Delay(Math.Max(0, delayMilliseconds), context.CancellationToken);
|
||||
yield return start + i;
|
||||
}
|
||||
}
|
||||
|
||||
[Export]
|
||||
[Cancellable]
|
||||
public async IAsyncEnumerable<int> PullForever(
|
||||
int delayMilliseconds,
|
||||
InvocationContext context)
|
||||
{
|
||||
var value = 0;
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(Math.Max(1, delayMilliseconds), context.CancellationToken);
|
||||
yield return value++;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Increment(ref _terminatedPullStreams);
|
||||
}
|
||||
}
|
||||
|
||||
[Export]
|
||||
[Cancellable]
|
||||
[Stream(StreamMode.Push, Pausable = true)]
|
||||
public AsyncReply<int> PushSequence(
|
||||
int count,
|
||||
int delayMilliseconds,
|
||||
InvocationContext context)
|
||||
{
|
||||
var reply = new AsyncReply<int>();
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(50, context.CancellationToken);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
await context.WaitWhileHaltedAsync();
|
||||
context.CancellationToken.ThrowIfCancellationRequested();
|
||||
reply.TriggerChunk(i);
|
||||
await Task.Delay(Math.Max(1, delayMilliseconds), context.CancellationToken);
|
||||
}
|
||||
|
||||
reply.Trigger(0);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// TerminateExecution completes the remote stream.
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
reply.TriggerError(exception);
|
||||
}
|
||||
});
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
|
||||
Copyright (c) 2017-2026 Ahmed Kh. Zamil
|
||||
|
||||
@@ -22,337 +22,338 @@ SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
using Esiur.Data;
|
||||
using Esiur.Core;
|
||||
using Esiur.Net.Http;
|
||||
using Esiur.Net.Sockets;
|
||||
using Esiur.Data;
|
||||
using Esiur.Data.Types;
|
||||
using Esiur.Protocol;
|
||||
using Esiur.Resource;
|
||||
using Esiur.Security.Authority;
|
||||
using Esiur.Security.Permissions;
|
||||
using Esiur.Security.RateLimiting;
|
||||
using Esiur.Stores;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Esiur.Security.Integrity;
|
||||
using System.Linq;
|
||||
using Esiur.Data.Types;
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Esiur.Proxy;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Esiur.Security.Cryptography;
|
||||
using Esiur.Security.Membership;
|
||||
using Esiur.Net.Packets;
|
||||
using System.Numerics;
|
||||
using Esiur.Protocol;
|
||||
using Esiur.Security.Authority.Providers;
|
||||
using Esiur.Security.Authority;
|
||||
#nullable enable
|
||||
|
||||
namespace Esiur.Tests.Functional;
|
||||
|
||||
|
||||
class Program
|
||||
internal static class Program
|
||||
{
|
||||
|
||||
static void TestSerialization(object x, EpConnection connection = null)
|
||||
static async Task Main()
|
||||
{
|
||||
var serverWarehouse = new Warehouse();
|
||||
var clientWarehouse = new Warehouse();
|
||||
EpConnection? connection = null;
|
||||
|
||||
var d = Codec.Compose(x, Warehouse.Default, connection);
|
||||
// var rr = DC.ToHex(y);
|
||||
|
||||
var y = Codec.ParseSync(d, 0, Warehouse.Default);
|
||||
Console.WriteLine($"{x.GetType().Name}: {x} == {y}, {d.ToHex()}");
|
||||
}
|
||||
|
||||
|
||||
[Export]
|
||||
public class StudentRecord : IRecord
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public byte Grade { get; set; }
|
||||
}
|
||||
public enum LogLevel : int
|
||||
{
|
||||
Debug,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
//TestSerialization("Hello");
|
||||
//TestSerialization(10);
|
||||
//TestSerialization(10.1);
|
||||
//TestSerialization(10.1d);
|
||||
//TestSerialization((byte)1);
|
||||
//TestSerialization((byte)2);
|
||||
//TestSerialization(new int[] { 1, 2, 3, 4 });
|
||||
//var x = LogLevel.Warning;
|
||||
|
||||
//TestSerialization(LogLevel.Warning);
|
||||
|
||||
//TestSerialization(new Map<string, byte?>
|
||||
//{
|
||||
// ["C++"] = 1,
|
||||
// ["C#"] = 2,
|
||||
// ["JS"] = null
|
||||
//});
|
||||
|
||||
|
||||
|
||||
//TestSerialization(new StudentRecord() { Name = "Ali", Grade = 90 });
|
||||
|
||||
//var tn = Encoding.UTF8.GetBytes("Test.StudentRecord");
|
||||
//var hash = System.Security.Cryptography.SHA256.Create().ComputeHash(tn).Clip(0, 16);
|
||||
//hash[6] = (byte)((hash[6] & 0xF) | 0x80);
|
||||
//hash[8] = (byte)((hash[8] & 0xF) | 0x80);
|
||||
|
||||
//var g = new UUID(hash);
|
||||
|
||||
//Console.WriteLine(g);
|
||||
|
||||
|
||||
|
||||
//var a = new ECDH();
|
||||
//var b = new ECDH();
|
||||
|
||||
//var apk = a.GetPublicKey();
|
||||
//var bpk = b.GetPublicKey();
|
||||
|
||||
//var ska = a.ComputeSharedKey(bpk);
|
||||
//var skb = b.ComputeSharedKey(apk);
|
||||
|
||||
//Console.WriteLine(ska.ToHex());
|
||||
//Console.WriteLine(skb.ToHex());
|
||||
|
||||
//// Simple membership provider
|
||||
//var membership = new SimpleMembership() { GuestsAllowed = true };
|
||||
|
||||
//membership.AddUser("user", "123456", new SimpleMembership.QuestionAnswer[0]);
|
||||
//membership.AddUser("admin", "admin", new SimpleMembership.QuestionAnswer[]
|
||||
//{
|
||||
// new SimpleMembership.QuestionAnswer()
|
||||
// {
|
||||
// Question = "What is 5+5",
|
||||
// Answer = 10,
|
||||
// Hashed = true,
|
||||
// }
|
||||
//});
|
||||
|
||||
var wh = new Warehouse();
|
||||
wh.RegisterAuthenticationProvider(new ServerAuthenticationProvider());
|
||||
|
||||
// Create stores to keep objects.
|
||||
var system = await wh.Put("sys", new MemoryStore());
|
||||
var server = await wh.Put("sys/server", new EpServer()
|
||||
try
|
||||
{
|
||||
AllowedAuthenticationProviders = new string[] { "hash" },
|
||||
var port = FindAvailablePort();
|
||||
var service = await StartServer(serverWarehouse, port);
|
||||
|
||||
connection = await ConnectClient(clientWarehouse, port);
|
||||
var remote = await connection.Get("sys/service") as EpResource
|
||||
?? throw new InvalidOperationException("Remote service was not found.");
|
||||
|
||||
await RunCoreScenarios(connection, service, remote);
|
||||
await RunRateControlScenarios(remote);
|
||||
await RunStreamingScenarios(service, remote);
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("All functional scenarios passed.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { connection?.Destroy(); } catch { }
|
||||
try { await clientWarehouse.Close(); } catch { }
|
||||
try { await serverWarehouse.Close(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<MyService> StartServer(Warehouse warehouse, ushort port)
|
||||
{
|
||||
warehouse.RegisterAuthenticationProvider(new ServerAuthenticationProvider());
|
||||
warehouse.Configuration.RateControl.DenialsBeforeConnectionBlock = 10;
|
||||
warehouse.AddRatePolicy(new BurstRatePolicy("standard-call")
|
||||
{
|
||||
PermitLimit = 1,
|
||||
Period = TimeSpan.FromMinutes(1),
|
||||
QueueLimit = 0,
|
||||
});
|
||||
warehouse.AddRatePolicy(new BurstRatePolicy("standard-set")
|
||||
{
|
||||
PermitLimit = 1,
|
||||
Period = TimeSpan.FromMinutes(1),
|
||||
QueueLimit = 0,
|
||||
});
|
||||
|
||||
|
||||
var web = await wh.Put("sys/web", new HttpServer() { Port = 8088 });
|
||||
|
||||
var service = await wh.Put("sys/service", new MyService());
|
||||
var res1 = await wh.Put("sys/service/r1", new MyResource() { Description = "Testing 1", CategoryId = 10 });
|
||||
var res2 = await wh.Put("sys/service/r2", new MyResource() { Description = "Testing 2", CategoryId = 11 });
|
||||
var res3 = await wh.Put("sys/service/c1", new MyChildResource() { ChildName = "Child 1", Description = "Child Testing 3", CategoryId = 12 });
|
||||
var res4 = await wh.Put("sys/service/c2", new MyChildResource() { ChildName = "Child 2 Destroy", Description = "Testing Destroy Handler", CategoryId = 12 });
|
||||
|
||||
//TestSerialization(res1);
|
||||
|
||||
server.MapCall("Hello", (string msg, DateTime time, EpConnection sender) =>
|
||||
await warehouse.Put("sys", new MemoryStore());
|
||||
var server = await warehouse.Put("sys/server", new EpServer
|
||||
{
|
||||
Console.WriteLine(msg);
|
||||
return "Hi " + DateTime.UtcNow;
|
||||
}).MapCall("temp", () => res4);
|
||||
|
||||
service.Resource = res1;
|
||||
service.ChildResource = res3;
|
||||
service.Resources = new MyResource[] { res1, res2, res1, res3 };
|
||||
service.MyResources = new MyResource[] { res1, res2, res3, res4 };
|
||||
|
||||
//web.MapGet("/{action}/{age}", (int age, string action, HTTPConnection sender) =>
|
||||
//{
|
||||
// Console.WriteLine($"AGE: {age} ACTION: {action}");
|
||||
|
||||
// sender.Response.Number = Esiur.Net.Packets.HTTPResponsePacket.ResponseCode.NotFound;
|
||||
// sender.Send("Not found");
|
||||
//});
|
||||
|
||||
web.MapGet("/", (HttpConnection sender) =>
|
||||
{
|
||||
sender.Send("Hello");
|
||||
Port = port,
|
||||
AllowedAuthenticationProviders = new[] { "hash" },
|
||||
});
|
||||
|
||||
await wh.Open();
|
||||
var service = await warehouse.Put("sys/service", new MyService());
|
||||
service.Instance!.Managers.Add(new AllowPropertySetPermissions());
|
||||
var resource1 = await warehouse.Put("sys/service/r1", new MyResource
|
||||
{
|
||||
Description = "Testing 1",
|
||||
CategoryId = 10,
|
||||
});
|
||||
var resource2 = await warehouse.Put("sys/service/r2", new MyResource
|
||||
{
|
||||
Description = "Testing 2",
|
||||
CategoryId = 11,
|
||||
});
|
||||
var child1 = await warehouse.Put("sys/service/c1", new MyChildResource
|
||||
{
|
||||
ChildName = "Child 1",
|
||||
Description = "Child Testing 3",
|
||||
CategoryId = 12,
|
||||
});
|
||||
var child2 = await warehouse.Put("sys/service/c2", new MyChildResource
|
||||
{
|
||||
ChildName = "Child 2",
|
||||
Description = "Testing lifecycle controls",
|
||||
CategoryId = 12,
|
||||
});
|
||||
|
||||
service.Resource = resource1;
|
||||
service.ChildResource = child1;
|
||||
service.Resources = new MyResource[] { resource1, resource2, resource1, child1 };
|
||||
service.MyResources = new MyResource[] { resource1, resource2, child1, child2 };
|
||||
|
||||
//var sc = service.GetGenericRecord();
|
||||
//var d = Codec.Compose(sc, Warehouse.Default, null);
|
||||
server.MapCall("Hello", (string message, DateTime _, EpConnection __) => $"Hi {message}");
|
||||
server.MapCall("temp", () => child2);
|
||||
|
||||
// Start testing
|
||||
TestClient(service);
|
||||
await warehouse.Open();
|
||||
return service;
|
||||
}
|
||||
|
||||
|
||||
// AuthorizationRequest, AsyncReply<object>
|
||||
static AsyncReply<object> Authenticator(AuthorizationRequest x)
|
||||
static async Task<EpConnection> ConnectClient(Warehouse warehouse, ushort port)
|
||||
{
|
||||
Console.WriteLine($"Authenticator: {x.Clue}");
|
||||
warehouse.RegisterAuthenticationProvider(new ClientAuthenticationProvider());
|
||||
|
||||
var format = x.RequiredFormat;
|
||||
|
||||
if (format == EpAuthPacketIAuthFormat.Number)
|
||||
return new AsyncReply<object>(Convert.ToInt32(10));
|
||||
else if (format == EpAuthPacketIAuthFormat.Text)
|
||||
return new AsyncReply<object>(Console.ReadLine().Trim());
|
||||
|
||||
throw new NotImplementedException("Not supported format.");
|
||||
}
|
||||
|
||||
private static async void TestClient(IResource local)
|
||||
{
|
||||
var wh = new Warehouse();
|
||||
var auth = new ClientAuthenticationProvider();
|
||||
wh.RegisterAuthenticationProvider(auth);
|
||||
|
||||
var con = await wh.Get<EpConnection>("ep://localhost", new EpConnectionContext
|
||||
return await warehouse.Get<EpConnection>($"ep://localhost:{port}", new EpConnectionContext
|
||||
{
|
||||
AuthenticationMode = AuthenticationMode.InitializerIdentity,
|
||||
AutoReconnect = true,
|
||||
AutoReconnect = false,
|
||||
Identity = "tester",
|
||||
AuthenticationProtocol = "hash",
|
||||
Domain = "test",
|
||||
});
|
||||
|
||||
|
||||
dynamic remote = await con.Get("sys/service");
|
||||
|
||||
TestObjectProps(local, remote);
|
||||
|
||||
//return;
|
||||
|
||||
//return;
|
||||
|
||||
Console.WriteLine("OK");
|
||||
|
||||
perodicTimer = new Timer(new TimerCallback(perodicTimerElapsed), remote, 0, 1000);
|
||||
|
||||
var pcall = await con.Call("Hello", "whats up ?", DateTime.UtcNow);
|
||||
|
||||
var temp = await con.Call("temp");
|
||||
Console.WriteLine("Temp: " + temp.GetHashCode());
|
||||
|
||||
|
||||
|
||||
|
||||
var opt = await remote.Optional(new { a1 = 22, a2 = 33, a4 = "What?" });
|
||||
Console.WriteLine(opt);
|
||||
|
||||
var hello = await remote.AsyncHello();
|
||||
|
||||
await remote.Void();
|
||||
await remote.Connection("ss", 33);
|
||||
await remote.ConnectionOptional("Test 2", 88);
|
||||
var rt = await remote.Optional("Optiona", 311);
|
||||
Console.WriteLine(rt);
|
||||
|
||||
var t2 = await remote.GetTuple2(1, "A");
|
||||
Console.WriteLine(t2);
|
||||
var t3 = await remote.GetTuple3(1, "A", 1.3);
|
||||
Console.WriteLine(t3);
|
||||
var t4 = await remote.GetTuple4(1, "A", 1.3, true);
|
||||
Console.WriteLine(t4);
|
||||
|
||||
remote.StringEvent += new EpResourceEvent((sender, args) =>
|
||||
Console.WriteLine($"StringEvent {args}")
|
||||
);
|
||||
|
||||
remote.ArrayEvent += new EpResourceEvent((sender, args) =>
|
||||
Console.WriteLine($"ArrayEvent {args}")
|
||||
);
|
||||
|
||||
await remote.InvokeEvents("Hello");
|
||||
|
||||
|
||||
|
||||
//var path = TemplateGenerator.GetTemplate("EP://localhost/sys/service", "Generated");
|
||||
|
||||
//Console.WriteLine(path);
|
||||
|
||||
|
||||
}
|
||||
|
||||
static async void perodicTimerElapsed(object state)
|
||||
static async Task RunCoreScenarios(EpConnection connection, MyService local, EpResource remote)
|
||||
{
|
||||
Console.WriteLine("Core RPC and serialization");
|
||||
ReportProperties(local, remote);
|
||||
|
||||
dynamic api = remote;
|
||||
|
||||
var procedureResult = (string)await connection.Call("Hello", "functional", DateTime.UtcNow);
|
||||
Require(procedureResult == "Hi functional", "Procedure call returned an unexpected value.");
|
||||
|
||||
var temporaryResource = (IResource)await connection.Call("temp");
|
||||
Require(
|
||||
temporaryResource is EpResource &&
|
||||
temporaryResource.Instance?.Definition.Name.EndsWith(nameof(MyChildResource), StringComparison.Ordinal) == true,
|
||||
"Procedure call did not return the expected remote resource type.");
|
||||
|
||||
var optional = await api.Optional(new { a1 = 22, a2 = 33, a4 = "What?" });
|
||||
Require(optional is double, "Optional argument invocation failed.");
|
||||
|
||||
var hello = await api.AsyncHello();
|
||||
Require(hello is not null, "AsyncReply invocation returned null.");
|
||||
|
||||
await api.Void();
|
||||
await api.Connection("connection", 33);
|
||||
await api.ConnectionOptional("optional connection", 88);
|
||||
|
||||
var tuple = await api.GetTuple4(1, "A", 1.3, true);
|
||||
Require(tuple is not null, "Tuple invocation returned null.");
|
||||
|
||||
var eventReceived = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
EpResourceEvent handler = (_, value) => eventReceived.TrySetResult((string)value);
|
||||
await remote.Subscribe(nameof(MyService.StringEvent));
|
||||
api.StringEvent += handler;
|
||||
|
||||
await api.InvokeEvents("event payload");
|
||||
var eventValue = await eventReceived.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
Require(eventValue == "event payload", "Remote event payload did not round-trip.");
|
||||
|
||||
api.StringEvent -= handler;
|
||||
await remote.Unsubscribe(nameof(MyService.StringEvent));
|
||||
Console.WriteLine(" PASS core RPC, optional arguments, tuples, and events");
|
||||
}
|
||||
|
||||
static async Task RunStreamingScenarios(MyService local, EpResource remote)
|
||||
{
|
||||
Console.WriteLine("Streaming and execution controls");
|
||||
|
||||
var pull = InvokeStream<int>(remote, nameof(MyService.PullRange), 10, 5, 5);
|
||||
var pulledValues = new List<int>();
|
||||
using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
|
||||
{
|
||||
await foreach (var value in pull.WithCancellation(timeout.Token))
|
||||
pulledValues.Add(value);
|
||||
}
|
||||
|
||||
Require(
|
||||
pulledValues.SequenceEqual(new[] { 10, 11, 12, 13, 14 }),
|
||||
"PullStream did not preserve item order or backpressure.");
|
||||
Console.WriteLine(" PASS PullStream / IAsyncEnumerable<T>");
|
||||
|
||||
var terminatedBefore = local.TerminatedPullStreams;
|
||||
var infinite = InvokeStream<int>(remote, nameof(MyService.PullForever), 5);
|
||||
var infiniteEnumerator = infinite.GetAsyncEnumerator();
|
||||
|
||||
Require(await infiniteEnumerator.MoveNextAsync(), "Infinite pull stream returned no first item.");
|
||||
Require(await infiniteEnumerator.MoveNextAsync(), "Infinite pull stream returned no second item.");
|
||||
await infiniteEnumerator.DisposeAsync();
|
||||
|
||||
await WaitUntil(
|
||||
() => local.TerminatedPullStreams == terminatedBefore + 1,
|
||||
TimeSpan.FromSeconds(5));
|
||||
Require(infinite.Completed, "TerminateExecution did not complete the client stream.");
|
||||
Console.WriteLine(" PASS TerminateExecution / enumerator disposal");
|
||||
|
||||
var push = InvokeStream<int>(remote, nameof(MyService.PushSequence), 4, 150);
|
||||
var pushEnumerator = push.GetAsyncEnumerator();
|
||||
var pushedValues = new List<int>();
|
||||
|
||||
Require(await pushEnumerator.MoveNextAsync(), "Push stream returned no first item.");
|
||||
pushedValues.Add(pushEnumerator.Current);
|
||||
|
||||
await push.Halt();
|
||||
var haltedMove = pushEnumerator.MoveNextAsync().AsTask();
|
||||
await Task.Delay(250);
|
||||
Require(!haltedMove.IsCompleted, "HaltExecution did not pause the producer.");
|
||||
|
||||
await push.Resume();
|
||||
Require(
|
||||
await haltedMove.WaitAsync(TimeSpan.FromSeconds(5)),
|
||||
"ResumeExecution did not resume the producer.");
|
||||
pushedValues.Add(pushEnumerator.Current);
|
||||
|
||||
while (await pushEnumerator.MoveNextAsync())
|
||||
pushedValues.Add(pushEnumerator.Current);
|
||||
|
||||
Require(
|
||||
pushedValues.SequenceEqual(new[] { 0, 1, 2, 3 }),
|
||||
"Push stream lost or reordered items across halt/resume.");
|
||||
Console.WriteLine(" PASS HaltExecution / ResumeExecution");
|
||||
}
|
||||
|
||||
static async Task RunRateControlScenarios(EpResource remote)
|
||||
{
|
||||
Console.WriteLine("Rate control");
|
||||
|
||||
var function = remote.Instance.Definition.GetFunctionDefByName(nameof(MyService.RateLimitedCall))
|
||||
?? throw new InvalidOperationException("Rate-limited function was not found.");
|
||||
var property = remote.Instance.Definition.GetPropertyDefByName(nameof(MyService.RateLimitedValue))
|
||||
?? throw new InvalidOperationException("Rate-limited property was not found.");
|
||||
|
||||
await remote._Invoke(function.Index, Array.Empty<object>());
|
||||
await ExpectRateLimit(() => remote._Invoke(function.Index, Array.Empty<object>()));
|
||||
|
||||
await remote.SetResourcePropertyAsync(property.Index, 1);
|
||||
await ExpectRateLimit(() => remote.SetResourcePropertyAsync(property.Index, 2));
|
||||
|
||||
Console.WriteLine(" PASS function-call and property-set policies");
|
||||
}
|
||||
|
||||
static async Task ExpectRateLimit(Func<AsyncReply> action)
|
||||
{
|
||||
GC.Collect();
|
||||
try
|
||||
{
|
||||
dynamic remote = state;
|
||||
await remote.InvokeEvents("Hello");
|
||||
|
||||
Console.WriteLine("Perodic : " + await remote.AsyncHello());
|
||||
await action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (AsyncException exception) when (exception.Code == ExceptionCode.RateLimitExceeded)
|
||||
{
|
||||
Console.WriteLine("Perodic : " + ex.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("The request was expected to be rate limited.");
|
||||
}
|
||||
|
||||
static Timer perodicTimer;
|
||||
|
||||
static void TestObjectProps(IResource local, EpResource remote)
|
||||
static AsyncStreamReply<T> InvokeStream<T>(
|
||||
EpResource resource,
|
||||
string functionName,
|
||||
params object[] arguments)
|
||||
{
|
||||
var function = resource.Instance.Definition.GetFunctionDefByName(functionName)
|
||||
?? throw new InvalidOperationException($"Function `{functionName}` was not found.");
|
||||
|
||||
foreach (var pt in local.Instance.Definition.Properties)
|
||||
{
|
||||
|
||||
var lv = pt.PropertyInfo.GetValue(local);
|
||||
object v;
|
||||
var rv = remote.TryGetPropertyValue(pt.Index, out v);
|
||||
if (!rv)
|
||||
Console.WriteLine($" ** {pt.Name} Failed");
|
||||
else
|
||||
Console.WriteLine($"{pt.Name} {GetString(lv)} == {GetString(v)}");
|
||||
}
|
||||
var indexedArguments = new Map<byte, object>();
|
||||
for (byte i = 0; i < arguments.Length; i++)
|
||||
indexedArguments[i] = arguments[i];
|
||||
|
||||
return resource._InvokeStream<T>(function.Index, indexedArguments);
|
||||
}
|
||||
|
||||
static string GetString(object value)
|
||||
static void ReportProperties(MyService local, EpResource remote)
|
||||
{
|
||||
if (value == null)
|
||||
return "NULL";
|
||||
var compared = 0;
|
||||
var definition = local.Instance?.Definition
|
||||
?? throw new InvalidOperationException("Local service was not initialized.");
|
||||
|
||||
var t = value.GetType();
|
||||
var nt = Nullable.GetUnderlyingType(t);
|
||||
if (nt != null)
|
||||
t = nt;
|
||||
if (t.IsArray)
|
||||
foreach (var property in definition.Properties)
|
||||
{
|
||||
var ar = (Array)value;
|
||||
if (ar.Length == 0)
|
||||
return "[]";
|
||||
var rt = "[";
|
||||
for (var i = 0; i < ar.Length - 1; i++)
|
||||
rt += GetString(ar.GetValue(i)) + ",";
|
||||
rt += GetString(ar.GetValue(ar.Length - 1)) + "]";
|
||||
if (!remote.TryGetPropertyValue(property.Index, out _))
|
||||
throw new InvalidOperationException($"Remote property `{property.Name}` was not attached.");
|
||||
|
||||
return rt;
|
||||
}
|
||||
else if (value is Record)
|
||||
{
|
||||
return value.ToString();
|
||||
}
|
||||
else if (value is IRecord)
|
||||
{
|
||||
return "{" + String.Join(", ", t.GetProperties().Select(x => x.Name + ": " + x.GetValue(value))) + "}";
|
||||
compared++;
|
||||
}
|
||||
|
||||
else
|
||||
return value.ToString();
|
||||
Console.WriteLine($" Attached {compared} exported properties");
|
||||
}
|
||||
}
|
||||
|
||||
static async Task WaitUntil(Func<bool> condition, TimeSpan timeout)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + timeout;
|
||||
while (!condition())
|
||||
{
|
||||
if (DateTime.UtcNow >= deadline)
|
||||
throw new TimeoutException("Timed out waiting for the functional condition.");
|
||||
|
||||
await Task.Delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
static void Require(bool condition, string message)
|
||||
{
|
||||
if (!condition)
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
static ushort FindAvailablePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = (ushort)((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
sealed class AllowPropertySetPermissions : IPermissionsManager
|
||||
{
|
||||
public Map<string, object> Settings { get; } = new();
|
||||
|
||||
public Ruling Applicable(
|
||||
IResource resource,
|
||||
Session session,
|
||||
ActionType action,
|
||||
MemberDef member,
|
||||
object inquirer = null!)
|
||||
=> action == ActionType.SetProperty ? Ruling.Allowed : Ruling.DontCare;
|
||||
|
||||
public bool Initialize(Map<string, object> settings, IResource resource) => true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Esiur functional smoke test
|
||||
|
||||
This project starts an in-process Esiur server and an authenticated client,
|
||||
runs its scenarios over a real loopback connection, and exits with a non-zero
|
||||
status if any assertion fails.
|
||||
|
||||
Coverage includes:
|
||||
|
||||
- resource attachment and property serialization;
|
||||
- procedure and resource calls, optional arguments, tuples, and events;
|
||||
- named rate policies for function calls and property setters;
|
||||
- rate-limit denial propagation to callers;
|
||||
- pull streams backed by `IAsyncEnumerable<T>`;
|
||||
- `TerminateExecution` through async-enumerator disposal;
|
||||
- `HaltExecution` and `ResumeExecution` for a cooperative push stream.
|
||||
|
||||
Run it from the repository root:
|
||||
|
||||
```powershell
|
||||
dotnet run --project Tests\Features\Functional\Esiur.Tests.Functional.csproj
|
||||
```
|
||||
|
||||
The runner selects an available loopback port and shuts down its client,
|
||||
server, and warehouses when complete.
|
||||
|
||||
Rate policies are registered by name on the server Warehouse:
|
||||
|
||||
```csharp
|
||||
warehouse.AddRatePolicy(new BurstRatePolicy("standard-call")
|
||||
{
|
||||
PermitLimit = 100,
|
||||
Period = TimeSpan.FromSeconds(1),
|
||||
BurstLimit = 20,
|
||||
QueueLimit = 50,
|
||||
});
|
||||
|
||||
warehouse.Configuration.RateControl.DenialsBeforeConnectionBlock = 5;
|
||||
|
||||
[RateControl("standard-call")]
|
||||
public void Call()
|
||||
{
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,159 @@
|
||||
using Esiur.Core;
|
||||
using Esiur.Data.Types;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
|
||||
public class AsyncStreamReplyTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task PullStream_RequestsOneItemPerMoveNext()
|
||||
{
|
||||
var pulls = 0;
|
||||
var stream = CreateStream<int>(StreamMode.Pull, pull: () =>
|
||||
{
|
||||
pulls++;
|
||||
return CompletedReply();
|
||||
});
|
||||
|
||||
stream.TriggerStreamStarted();
|
||||
var enumerator = stream.GetAsyncEnumerator();
|
||||
|
||||
var firstMove = enumerator.MoveNextAsync().AsTask();
|
||||
Assert.Equal(1, pulls);
|
||||
Assert.False(firstMove.IsCompleted);
|
||||
|
||||
stream.TriggerChunk(17);
|
||||
Assert.True(await firstMove);
|
||||
Assert.Equal(17, enumerator.Current);
|
||||
|
||||
var secondMove = enumerator.MoveNextAsync().AsTask();
|
||||
Assert.Equal(2, pulls);
|
||||
stream.TriggerStreamCompleted();
|
||||
|
||||
Assert.False(await secondMove);
|
||||
await enumerator.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposingStream_TerminatesRemoteExecutionOnce()
|
||||
{
|
||||
var terminations = 0;
|
||||
var stream = CreateStream<int>(StreamMode.Pull, terminate: () =>
|
||||
{
|
||||
terminations++;
|
||||
return CompletedReply();
|
||||
});
|
||||
|
||||
stream.TriggerStreamStarted();
|
||||
var enumerator = stream.GetAsyncEnumerator();
|
||||
|
||||
await enumerator.DisposeAsync();
|
||||
await enumerator.DisposeAsync();
|
||||
|
||||
Assert.Equal(1, terminations);
|
||||
Assert.True(stream.Completed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LifecycleControls_AreSentThroughStreamReply()
|
||||
{
|
||||
var halts = 0;
|
||||
var resumes = 0;
|
||||
var stream = CreateStream<int>(
|
||||
StreamMode.Push,
|
||||
halt: () =>
|
||||
{
|
||||
halts++;
|
||||
return CompletedReply();
|
||||
},
|
||||
resume: () =>
|
||||
{
|
||||
resumes++;
|
||||
return CompletedReply();
|
||||
});
|
||||
|
||||
stream.TriggerStreamStarted();
|
||||
stream.Halt();
|
||||
stream.Resume();
|
||||
|
||||
Assert.Equal(1, halts);
|
||||
Assert.Equal(1, resumes);
|
||||
Assert.Throws<InvalidOperationException>(() => stream.Pull());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvocationContext_PullsGenericAsyncEnumerableAndCancelsIt()
|
||||
{
|
||||
var disposed = false;
|
||||
var context = new InvocationContext(null!, 1);
|
||||
context.InitializeStream(StreamMode.Pull, pausable: false);
|
||||
|
||||
Assert.True(context.SetAsyncEnumerable(Values(() => disposed = true)));
|
||||
|
||||
var first = await context.PullAsync();
|
||||
var second = await context.PullAsync();
|
||||
|
||||
Assert.True(first.HasValue);
|
||||
Assert.Equal(1, first.Value);
|
||||
Assert.True(second.HasValue);
|
||||
Assert.Equal(2, second.Value);
|
||||
|
||||
await context.TerminateAsync();
|
||||
|
||||
Assert.True(context.CancellationToken.IsCancellationRequested);
|
||||
Assert.True(disposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvocationContext_HaltAndResumeGatePushEnumeration()
|
||||
{
|
||||
var context = new InvocationContext(null!, 1);
|
||||
context.InitializeStream(StreamMode.Push, pausable: true);
|
||||
context.SetEnumerable(new[] { 3, 4 });
|
||||
|
||||
Assert.True(context.Halt());
|
||||
var move = context.MoveNextAsync();
|
||||
Assert.False(move.IsCompleted);
|
||||
|
||||
Assert.True(context.Resume());
|
||||
var item = await move;
|
||||
|
||||
Assert.True(item.HasValue);
|
||||
Assert.Equal(3, item.Value);
|
||||
await context.EndAsync();
|
||||
}
|
||||
|
||||
static AsyncStreamReply<T> CreateStream<T>(
|
||||
StreamMode mode,
|
||||
Func<AsyncReply>? pull = null,
|
||||
Func<AsyncReply>? terminate = null,
|
||||
Func<AsyncReply>? halt = null,
|
||||
Func<AsyncReply>? resume = null)
|
||||
=> new(
|
||||
mode,
|
||||
pull ?? CompletedReply,
|
||||
terminate ?? CompletedReply,
|
||||
halt ?? CompletedReply,
|
||||
resume ?? CompletedReply);
|
||||
|
||||
static AsyncReply CompletedReply() => new((object)null!);
|
||||
|
||||
static async IAsyncEnumerable<int> Values(
|
||||
Action onDisposed,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
yield return 1;
|
||||
await Task.Yield();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return 2;
|
||||
await Task.Delay(Timeout.Infinite, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
onDisposed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using Esiur.Core;
|
||||
using Esiur.Data;
|
||||
using Esiur.Protocol;
|
||||
|
||||
namespace Esiur.Tests.Unit.Integration;
|
||||
|
||||
[Collection("Integration")]
|
||||
public class AsyncStreamIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AsyncEnumerable_IsPulledAcrossProtocol()
|
||||
{
|
||||
await using var cluster = await StartCluster().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var remote = await Task.Run(async () =>
|
||||
(EpResource)await cluster.Connection.Get("sys/stream"))
|
||||
.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var function = remote.Instance.Definition.GetFunctionDefByName(nameof(StreamResource.Numbers));
|
||||
var stream = remote._InvokeStream<int>(
|
||||
function.Index,
|
||||
new Map<byte, object> { [0] = 4 });
|
||||
|
||||
var values = new List<int>();
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
await foreach (var value in stream.WithCancellation(timeout.Token))
|
||||
values.Add(value);
|
||||
|
||||
Assert.Equal(new[] { 0, 1, 2, 3 }, values);
|
||||
Assert.True(stream.Completed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposingAsyncEnumerable_TerminatesRemoteExecution()
|
||||
{
|
||||
await using var cluster = await StartCluster().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var remote = await Task.Run(async () =>
|
||||
(EpResource)await cluster.Connection.Get("sys/stream"))
|
||||
.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var function = remote.Instance.Definition.GetFunctionDefByName(nameof(StreamResource.Infinite));
|
||||
var stream = remote._InvokeStream<int>(function.Index, Array.Empty<object>());
|
||||
var enumerator = stream.GetAsyncEnumerator();
|
||||
|
||||
Assert.True(await enumerator.MoveNextAsync());
|
||||
Assert.Equal(0, enumerator.Current);
|
||||
|
||||
await enumerator.DisposeAsync();
|
||||
Assert.True(stream.Completed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PausablePushStream_HaltsAndResumesAcrossProtocol()
|
||||
{
|
||||
await using var cluster = await StartCluster().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var remote = await Task.Run(async () =>
|
||||
(EpResource)await cluster.Connection.Get("sys/stream"))
|
||||
.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var function = remote.Instance.Definition.GetFunctionDefByName(nameof(StreamResource.Pausable));
|
||||
var stream = remote._InvokeStream<int>(function.Index, Array.Empty<object>());
|
||||
var enumerator = stream.GetAsyncEnumerator();
|
||||
|
||||
Assert.True(await enumerator.MoveNextAsync());
|
||||
Assert.Equal(0, enumerator.Current);
|
||||
|
||||
await stream.Halt();
|
||||
var haltedMove = enumerator.MoveNextAsync().AsTask();
|
||||
await Task.Delay(200);
|
||||
Assert.False(haltedMove.IsCompleted);
|
||||
|
||||
await stream.Resume();
|
||||
Assert.True(await haltedMove.WaitAsync(TimeSpan.FromSeconds(5)));
|
||||
Assert.Equal(1, enumerator.Current);
|
||||
|
||||
await enumerator.DisposeAsync();
|
||||
}
|
||||
|
||||
static Task<IntegrationCluster> StartCluster()
|
||||
=> IntegrationCluster.StartAsync(async warehouse =>
|
||||
{
|
||||
await warehouse.Put("sys/stream", new StreamResource());
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using Esiur.Core;
|
||||
using Esiur.Data;
|
||||
using Esiur.Data.Types;
|
||||
using Esiur.Protocol;
|
||||
using Esiur.Resource;
|
||||
using Esiur.Security.Authority;
|
||||
using Esiur.Security.Permissions;
|
||||
using Esiur.Security.RateLimiting;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Esiur.Tests.Unit.Integration;
|
||||
|
||||
[Collection("Integration")]
|
||||
public class RateControlIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task FunctionCalls_AreDeniedWhenPolicyIsExhausted()
|
||||
{
|
||||
await using var cluster = await StartCluster().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var remote = await GetRemote(cluster);
|
||||
var function = remote.Instance.Definition.GetFunctionDefByName(nameof(RateLimitedResource.Call));
|
||||
|
||||
var first = await remote._Invoke(function.Index, Array.Empty<object>());
|
||||
var exception = await Assert.ThrowsAsync<AsyncException>(async () =>
|
||||
await remote._Invoke(function.Index, Array.Empty<object>()));
|
||||
|
||||
Assert.Equal(1, Convert.ToInt32(first));
|
||||
Assert.Equal(ExceptionCode.RateLimitExceeded, exception.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PropertySets_AreDeniedWhenPolicyIsExhausted()
|
||||
{
|
||||
await using var cluster = await StartCluster().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var remote = await GetRemote(cluster);
|
||||
var property = remote.Instance.Definition.GetPropertyDefByName(nameof(RateLimitedResource.Value));
|
||||
|
||||
await remote.SetResourcePropertyAsync(property.Index, 10);
|
||||
var exception = await Assert.ThrowsAsync<AsyncException>(async () =>
|
||||
await remote.SetResourcePropertyAsync(property.Index, 20));
|
||||
|
||||
Assert.Equal(ExceptionCode.RateLimitExceeded, exception.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RepeatedDenials_BlockTheConnection()
|
||||
{
|
||||
await using var cluster = await StartCluster(denialsBeforeBlock: 1)
|
||||
.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
cluster.Connection.AutoReconnect = false;
|
||||
var remote = await GetRemote(cluster);
|
||||
var function = remote.Instance.Definition.GetFunctionDefByName(nameof(RateLimitedResource.Call));
|
||||
|
||||
await remote._Invoke(function.Index, Array.Empty<object>());
|
||||
var exception = await Assert.ThrowsAsync<AsyncException>(async () =>
|
||||
await remote._Invoke(function.Index, Array.Empty<object>()));
|
||||
|
||||
Assert.Equal(ExceptionCode.RateLimitExceeded, exception.Code);
|
||||
await WaitUntilAsync(() => !cluster.Connection.IsConnected, TimeSpan.FromSeconds(3));
|
||||
Assert.False(cluster.Connection.IsConnected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueuedCalls_AreDelayedAndQueueOverflowIsDenied()
|
||||
{
|
||||
await using var cluster = await StartCluster(
|
||||
queueLimit: 1,
|
||||
period: TimeSpan.FromMilliseconds(250)).WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var remote = await GetRemote(cluster);
|
||||
var function = remote.Instance.Definition.GetFunctionDefByName(nameof(RateLimitedResource.Call));
|
||||
|
||||
await remote._Invoke(function.Index, Array.Empty<object>());
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var queued = remote._Invoke(function.Index, Array.Empty<object>());
|
||||
var exception = await Assert.ThrowsAsync<AsyncException>(async () =>
|
||||
await remote._Invoke(function.Index, Array.Empty<object>()));
|
||||
var result = await queued;
|
||||
|
||||
Assert.Equal(ExceptionCode.RateLimitExceeded, exception.Code);
|
||||
Assert.Equal(2, Convert.ToInt32(result));
|
||||
Assert.True(stopwatch.Elapsed >= TimeSpan.FromMilliseconds(150));
|
||||
}
|
||||
|
||||
static Task<IntegrationCluster> StartCluster(
|
||||
int denialsBeforeBlock = 10,
|
||||
int queueLimit = 0,
|
||||
TimeSpan? period = null)
|
||||
=> IntegrationCluster.StartAsync(async warehouse =>
|
||||
{
|
||||
warehouse.Configuration.RateControl.DenialsBeforeConnectionBlock = denialsBeforeBlock;
|
||||
warehouse.Configuration.RateControl.ConnectionBlockDelay = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
warehouse.AddRatePolicy(new BurstRatePolicy("standard-call")
|
||||
{
|
||||
PermitLimit = 1,
|
||||
Period = period ?? TimeSpan.FromMinutes(1),
|
||||
QueueLimit = queueLimit,
|
||||
});
|
||||
warehouse.AddRatePolicy(new BurstRatePolicy("standard-set")
|
||||
{
|
||||
PermitLimit = 1,
|
||||
Period = period ?? TimeSpan.FromMinutes(1),
|
||||
QueueLimit = queueLimit,
|
||||
});
|
||||
|
||||
var resource = await warehouse.Put("sys/rate", new RateLimitedResource());
|
||||
resource.Instance!.Managers.Add(new AllowPropertySetPermissions());
|
||||
});
|
||||
|
||||
static async Task<EpResource> GetRemote(IntegrationCluster cluster)
|
||||
=> await Task.Run(async () =>
|
||||
(EpResource)await cluster.Connection.Get("sys/rate"))
|
||||
.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
|
||||
static async Task WaitUntilAsync(Func<bool> condition, TimeSpan timeout)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + timeout;
|
||||
while (!condition())
|
||||
{
|
||||
if (DateTime.UtcNow >= deadline)
|
||||
throw new TimeoutException("The expected condition was not reached.");
|
||||
await Task.Delay(20);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AllowPropertySetPermissions : IPermissionsManager
|
||||
{
|
||||
public Map<string, object> Settings { get; } = new();
|
||||
|
||||
public Ruling Applicable(
|
||||
IResource resource,
|
||||
Session session,
|
||||
ActionType action,
|
||||
MemberDef member,
|
||||
object inquirer = null!)
|
||||
=> action == ActionType.SetProperty ? Ruling.Allowed : Ruling.DontCare;
|
||||
|
||||
public bool Initialize(Map<string, object> settings, IResource resource) => true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Esiur.Resource;
|
||||
using System.Threading;
|
||||
|
||||
namespace Esiur.Tests.Unit.Integration;
|
||||
|
||||
[Resource]
|
||||
public partial class RateLimitedResource
|
||||
{
|
||||
int _callCount;
|
||||
int _value;
|
||||
|
||||
[Export]
|
||||
[RateControl("standard-call")]
|
||||
public int Call() => Interlocked.Increment(ref _callCount);
|
||||
|
||||
[Export]
|
||||
[RateControl("standard-set")]
|
||||
public int Value
|
||||
{
|
||||
get => _value;
|
||||
set => _value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Esiur.Core;
|
||||
using Esiur.Data.Types;
|
||||
using Esiur.Resource;
|
||||
|
||||
namespace Esiur.Tests.Unit.Integration;
|
||||
|
||||
[Resource]
|
||||
public partial class StreamResource
|
||||
{
|
||||
[Export]
|
||||
public async IAsyncEnumerable<int> Numbers(int count, InvocationContext context)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
await Task.Delay(5, context.CancellationToken);
|
||||
yield return i;
|
||||
}
|
||||
}
|
||||
|
||||
[Export]
|
||||
public async IAsyncEnumerable<int> Infinite(InvocationContext context)
|
||||
{
|
||||
var value = 0;
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(5, context.CancellationToken);
|
||||
yield return value++;
|
||||
}
|
||||
}
|
||||
|
||||
[Export]
|
||||
[Stream(StreamMode.Push, Pausable = true)]
|
||||
public AsyncReply<int> Pausable(InvocationContext context)
|
||||
{
|
||||
var reply = new AsyncReply<int>();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100);
|
||||
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
await context.WaitWhileHaltedAsync();
|
||||
reply.TriggerChunk(i);
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
reply.Trigger(0);
|
||||
});
|
||||
|
||||
return reply;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using Esiur.Data.Types;
|
||||
using Esiur.Protocol;
|
||||
using Esiur.Resource;
|
||||
using Esiur.Security.Authority;
|
||||
using Esiur.Security.Permissions;
|
||||
using Esiur.Security.RateLimiting;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
|
||||
public class RatePolicyTests
|
||||
{
|
||||
[Fact]
|
||||
public void RateControlAttribute_IsCapturedForFunctionsAndProperties()
|
||||
{
|
||||
var warehouse = new Warehouse();
|
||||
var method = typeof(RateFixture).GetMethod(nameof(RateFixture.Call))!;
|
||||
var property = typeof(RateFixture).GetProperty(nameof(RateFixture.Value))!;
|
||||
|
||||
var functionDefinition = FunctionDef.MakeFunctionDef(
|
||||
warehouse, typeof(RateFixture), method, 0, method.Name, new TypeDef());
|
||||
var propertyDefinition = PropertyDef.MakePropertyDef(
|
||||
warehouse, typeof(RateFixture), property, property.Name, 0, new TypeDef());
|
||||
|
||||
Assert.Equal("standard-call", functionDefinition.RatePolicyName);
|
||||
Assert.Equal("standard-set", propertyDefinition.RatePolicyName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Warehouse_RegistersPoliciesByTheirConfiguredName()
|
||||
{
|
||||
var warehouse = new Warehouse();
|
||||
var policy = new DenyPolicy { Name = "deny" };
|
||||
|
||||
warehouse.AddRatePolicy(policy);
|
||||
|
||||
Assert.Same(policy, warehouse.TryGetRatePolicy("deny"));
|
||||
Assert.Throws<InvalidOperationException>(() => warehouse.AddRatePolicy(policy));
|
||||
Assert.True(warehouse.RemoveRatePolicy("deny"));
|
||||
Assert.Null(warehouse.TryGetRatePolicy("deny"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContextFreePolicies_AreSupported()
|
||||
{
|
||||
var policy = new DenyPolicy();
|
||||
Assert.Equal(Ruling.Denied, policy.Applicable(CreateContext()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BurstPolicy_AllowsBurstQueuesOverflowAndThenDenies()
|
||||
{
|
||||
var policy = new BurstRatePolicy("standard-call")
|
||||
{
|
||||
PermitLimit = 1,
|
||||
Period = TimeSpan.FromSeconds(1),
|
||||
BurstLimit = 1,
|
||||
QueueLimit = 1,
|
||||
};
|
||||
|
||||
var immediateOne = CreateContext();
|
||||
var immediateTwo = CreateContext(immediateOne.Connection);
|
||||
var queued = CreateContext(immediateOne.Connection);
|
||||
var denied = CreateContext(immediateOne.Connection);
|
||||
|
||||
Assert.Equal(Ruling.Allowed, policy.Applicable(immediateOne));
|
||||
Assert.Equal(Ruling.Allowed, policy.Applicable(immediateTwo));
|
||||
Assert.Equal(Ruling.Allowed, policy.Applicable(queued));
|
||||
Assert.True(queued.Delay > TimeSpan.Zero);
|
||||
Assert.Equal(Ruling.Denied, policy.Applicable(denied));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarehouseConfiguration_IsIsolatedPerWarehouse()
|
||||
{
|
||||
var configured = new Warehouse(new WarehouseConfiguration
|
||||
{
|
||||
RateControl = new RateControlConfiguration
|
||||
{
|
||||
DenialsBeforeConnectionBlock = 2,
|
||||
DenialWindow = TimeSpan.FromSeconds(10),
|
||||
ConnectionBlockDelay = TimeSpan.Zero,
|
||||
},
|
||||
});
|
||||
var defaults = new Warehouse();
|
||||
|
||||
Assert.Equal(2, configured.Configuration.RateControl.DenialsBeforeConnectionBlock);
|
||||
Assert.Equal(5, defaults.Configuration.RateControl.DenialsBeforeConnectionBlock);
|
||||
}
|
||||
|
||||
static RateControlContext CreateContext(EpConnection? connection = null)
|
||||
=> new(
|
||||
new Warehouse(),
|
||||
connection ?? new EpConnection(),
|
||||
new Session(),
|
||||
null,
|
||||
new FunctionDef { Name = "Call" },
|
||||
ActionType.Execute);
|
||||
|
||||
sealed class DenyPolicy : RatePolicy
|
||||
{
|
||||
public override Ruling Applicable() => Ruling.Denied;
|
||||
}
|
||||
|
||||
sealed class RateFixture
|
||||
{
|
||||
[RateControl("standard-call")]
|
||||
public void Call()
|
||||
{
|
||||
}
|
||||
|
||||
[RateControl("standard-set")]
|
||||
public int Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
using Esiur.Core;
|
||||
using Esiur.Data;
|
||||
using Esiur.Data.Types;
|
||||
using Esiur.Resource;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
|
||||
public class TypeDefAttributeTests
|
||||
{
|
||||
[Fact]
|
||||
public void FunctionStreamMode_IsInferredOrReadFromAttribute()
|
||||
{
|
||||
var pull = MakeFunction(nameof(StreamFixture.Pull));
|
||||
var push = MakeFunction(nameof(StreamFixture.Push));
|
||||
var explicitStream = MakeFunction(nameof(StreamFixture.Explicit));
|
||||
|
||||
Assert.Equal(StreamMode.Pull, pull.StreamMode);
|
||||
Assert.Equal(TruIdentifier.Int32, pull.ReturnType.Identifier);
|
||||
Assert.Equal(StreamMode.Push, push.StreamMode);
|
||||
Assert.Equal(TruIdentifier.Int32, push.ReturnType.Identifier);
|
||||
Assert.Equal(StreamMode.Push, explicitStream.StreamMode);
|
||||
Assert.True(explicitStream.Pausable);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(StreamFixture.InvalidReturn))]
|
||||
[InlineData(nameof(StreamFixture.InvalidPausablePull))]
|
||||
[InlineData(nameof(StreamFixture.ConflictingMode))]
|
||||
public void FunctionStreamMode_RejectsInvalidDeclarations(string methodName)
|
||||
{
|
||||
var exception = Assert.Throws<Exception>(() => MakeFunction(methodName));
|
||||
Assert.Contains("stream", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Attributes_AreIncludedInSerializedTypeDef()
|
||||
{
|
||||
var warehouse = new Warehouse();
|
||||
var definition = new LocalTypeDef(typeof(AttributedResource), warehouse);
|
||||
var bytes = Codec.Compose(TypeDefInfo.FromTypeDef(definition), warehouse, null);
|
||||
var (_, info) = Codec.ParseIndexedType<TypeDefInfo>(bytes, 0, warehouse);
|
||||
|
||||
Assert.Equal("type usage", info.Usage);
|
||||
Assert.Equal("type description", info.Description);
|
||||
Assert.Equal("type example", info.Example);
|
||||
Assert.Equal("tests", info.Category);
|
||||
Assert.Equal("3.1", info.Since);
|
||||
Assert.Equal("type", info.Annotations!["scope"]);
|
||||
|
||||
var property = Assert.Single(info.Properties!, x => x.Name == "Value");
|
||||
var propertyFlags = (PropertyDefFlags)property.Flags;
|
||||
Assert.True(propertyFlags.HasFlag(PropertyDefFlags.ReadOnly));
|
||||
Assert.True(propertyFlags.HasFlag(PropertyDefFlags.Volatile));
|
||||
Assert.True(propertyFlags.HasFlag(PropertyDefFlags.Historical));
|
||||
Assert.True(propertyFlags.HasFlag(PropertyDefFlags.Deprecated));
|
||||
Assert.Equal(OrderingControl.LatestOnly, property.OrderingControl);
|
||||
Assert.Equal(5, Convert.ToInt32(property.DefaultValue));
|
||||
Assert.Equal("property description", property.Description);
|
||||
Assert.Equal("property usage", property.Usage);
|
||||
Assert.Equal(2, property.Examples!.Count);
|
||||
Assert.Equal(new[] { "state", "sample" }, property.Tags);
|
||||
Assert.Equal("units", property.Unit);
|
||||
Assert.Equal(0, Convert.ToInt32(property.Minimum));
|
||||
Assert.Equal(10, Convert.ToInt32(property.Maximum));
|
||||
Assert.Equal(new[] { 1, 5 }, property.AllowedValues!.Select(Convert.ToInt32));
|
||||
Assert.Equal("^[0-9]+$", property.Pattern);
|
||||
Assert.Equal("integer", property.Format);
|
||||
Assert.Equal("property warning", Assert.Single(property.Warnings!));
|
||||
Assert.Equal((byte)7, Assert.Single(property.RelatedMembers!));
|
||||
Assert.Equal("use NewValue", property.DeprecationMessage);
|
||||
Assert.Equal("property", property.Annotations!["scope"]);
|
||||
|
||||
var function = Assert.Single(info.Functions!, x => x.Name == nameof(AttributedResource.Watch));
|
||||
var functionFlags = (FunctionDefFlags)function.Flags;
|
||||
Assert.True(functionFlags.HasFlag(FunctionDefFlags.ReadOnly));
|
||||
Assert.True(functionFlags.HasFlag(FunctionDefFlags.Idempotent));
|
||||
Assert.True(functionFlags.HasFlag(FunctionDefFlags.Cancellable));
|
||||
Assert.True(functionFlags.HasFlag(FunctionDefFlags.Pausable));
|
||||
Assert.Equal(StreamMode.Push, function.StreamMode);
|
||||
Assert.Equal("ready", Assert.Single(function.Preconditions!));
|
||||
Assert.Equal("complete", Assert.Single(function.Postconditions!));
|
||||
Assert.Equal(OperationEffects.EmitsEvents, function.Effects);
|
||||
|
||||
var eventInfo = Assert.Single(info.Events!, x => x.Name == nameof(AttributedResource.Changed));
|
||||
var eventFlags = (EventDefFlags)eventInfo.Flags;
|
||||
Assert.True(eventFlags.HasFlag(EventDefFlags.AutoDelivered));
|
||||
Assert.True(eventFlags.HasFlag(EventDefFlags.Historical));
|
||||
Assert.Equal(OrderingControl.Relaxed, eventInfo.OrderingControl);
|
||||
|
||||
var constant = Assert.Single(info.Constants!, x => x.Name == nameof(AttributedResource.Limit));
|
||||
Assert.Equal("constant description", constant.Description);
|
||||
Assert.Equal("constant usage", constant.Usage);
|
||||
Assert.Equal(10, Convert.ToInt32(constant.Value));
|
||||
}
|
||||
|
||||
private static FunctionDef MakeFunction(string name)
|
||||
{
|
||||
var method = typeof(StreamFixture).GetMethod(name, BindingFlags.Public | BindingFlags.Instance)!;
|
||||
return FunctionDef.MakeFunctionDef(
|
||||
Warehouse.Default, typeof(StreamFixture), method, 0, name, new TypeDef());
|
||||
}
|
||||
|
||||
private sealed class StreamFixture
|
||||
{
|
||||
public IAsyncEnumerable<int> Pull() => throw new NotSupportedException();
|
||||
|
||||
public IEnumerable<int> Push() => Array.Empty<int>();
|
||||
|
||||
[Stream(StreamMode.Push, Pausable = true)]
|
||||
public AsyncReply<int> Explicit() => new(1);
|
||||
|
||||
[Stream]
|
||||
public int InvalidReturn() => 1;
|
||||
|
||||
[Stream(StreamMode.Pull, Pausable = true)]
|
||||
public AsyncReply<int> InvalidPausablePull() => new(1);
|
||||
|
||||
[Stream(StreamMode.Push)]
|
||||
public IAsyncEnumerable<int> ConflictingMode() => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
[Export]
|
||||
[Usage("type usage")]
|
||||
[Description("type description")]
|
||||
[Example("type example")]
|
||||
[Category("tests")]
|
||||
[Since("3.1")]
|
||||
[Annotation("scope", "type")]
|
||||
private sealed class AttributedResource : Esiur.Resource.Resource
|
||||
{
|
||||
[Description("constant description")]
|
||||
[Usage("constant usage")]
|
||||
[Example(10)]
|
||||
[Annotation("scope", "constant")]
|
||||
public const int Limit = 10;
|
||||
|
||||
[ReadOnly]
|
||||
[Volatile]
|
||||
[Historical]
|
||||
[Ordering(OrderingControl.LatestOnly)]
|
||||
[System.ComponentModel.DefaultValue(5)]
|
||||
[Description("property description")]
|
||||
[Usage("property usage")]
|
||||
[Example(1)]
|
||||
[Example(5)]
|
||||
[Tags("state", "sample")]
|
||||
[Unit("units")]
|
||||
[Minimum(0)]
|
||||
[Maximum(10)]
|
||||
[AllowedValue(1)]
|
||||
[AllowedValue(5)]
|
||||
[Pattern("^[0-9]+$")]
|
||||
[Format("integer")]
|
||||
[Warning("property warning")]
|
||||
[RelatedMembers(7)]
|
||||
[Obsolete("use NewValue")]
|
||||
[Annotation("scope", "property")]
|
||||
public int Value { get; set; }
|
||||
|
||||
[Stream(StreamMode.Push, Pausable = true)]
|
||||
[ReadOnly]
|
||||
[Idempotent]
|
||||
[Cancellable]
|
||||
[Precondition("ready")]
|
||||
[Postcondition("complete")]
|
||||
[Effects(OperationEffects.EmitsEvents)]
|
||||
public AsyncReply<int> Watch() => new(1);
|
||||
|
||||
[AutoDelivery]
|
||||
[Historical]
|
||||
[Ordering(OrderingControl.Relaxed)]
|
||||
public event ResourceEventHandler<int>? Changed;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user