111111
This commit is contained in:
@@ -2,7 +2,9 @@ using Admin.NET.Core;
|
||||
using Evergreen.Health.Application.Health.Entities;
|
||||
using Evergreen.Health.Application.Health.Integrations.Ib43;
|
||||
using Evergreen.Health.Application.Health.Services;
|
||||
using Evergreen.Health.Application.Health.Sleep;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SqlSugar;
|
||||
using System.Security.Claims;
|
||||
@@ -12,6 +14,7 @@ namespace Admin.NET.Test.Health;
|
||||
|
||||
public sealed class Ib43PersistenceTests : IDisposable
|
||||
{
|
||||
private const string PreviewAction = "dehumidifier-humidity-preview";
|
||||
private readonly SqlSugarClient _db;
|
||||
private readonly string _databasePath = Path.Combine(Path.GetTempPath(), "ib43-test-" + Guid.NewGuid().ToString("N") + ".db");
|
||||
private readonly Ib43DataRepository _repository;
|
||||
@@ -19,6 +22,7 @@ public sealed class Ib43PersistenceTests : IDisposable
|
||||
private readonly Ib43Binding _device = new() { ProductKey = "test", SerialNumber = "sim", Simulation = true };
|
||||
private readonly HttpContextAccessor _context = new() { HttpContext = new DefaultHttpContext() };
|
||||
private readonly HealthIb43Service _service;
|
||||
private readonly BedPresenceAutomation _automation;
|
||||
|
||||
private sealed class Clock : TimeProvider
|
||||
{
|
||||
@@ -37,14 +41,31 @@ public sealed class Ib43PersistenceTests : IDisposable
|
||||
if (Nullable.GetUnderlyingType(property.PropertyType) != null) column.IsNullable = true;
|
||||
} } });
|
||||
_db.CodeFirst.InitTables<HealthIb43Binding, HealthIb43Observation, HealthUser>();
|
||||
_db.CodeFirst.InitTables<HealthIb43AutomationExecution>();
|
||||
_db.Ado.ExecuteCommand("CREATE TABLE SysUser (Id INTEGER PRIMARY KEY, AccountType INTEGER, Status INTEGER)");
|
||||
_db.Ado.ExecuteCommand("INSERT INTO SysUser VALUES (99,999,1)");
|
||||
foreach (var id in new[] { 1L, 2L, 3L })
|
||||
_db.Insertable(new HealthUser { Id = id, Phone = id.ToString(), DisplayName = "test", PasswordHash = "unused",
|
||||
CreateTime = DateTime.UtcNow, CreateUserName = "test", UpdateUserName = "test" }).ExecuteCommand();
|
||||
_repository = new(_db);
|
||||
var options = Options.Create(new Ib43Options { Devices = [_device] });
|
||||
_service = new(_repository, _context, options, new(options, new(), _clock), _clock);
|
||||
_automation = CreateAutomation(_repository, PreviewAction);
|
||||
_service = CreateService(_repository, _automation);
|
||||
}
|
||||
|
||||
private BedPresenceAutomation CreateAutomation(Ib43DataRepository repository, string secondaryAction) =>
|
||||
new(Options.Create(new BedPresenceAutomationOptions
|
||||
{
|
||||
Mode = BedAutomationMode.Managed, ProductKey = _device.ProductKey, SerialNumber = _device.SerialNumber,
|
||||
ActionKey = "curtain-close", TargetEntityId = "cover.test", TargetLabel = "测试窗帘",
|
||||
AdditionalRules = [new() { Mode = BedAutomationMode.Managed, ProductKey = _device.ProductKey,
|
||||
SerialNumber = _device.SerialNumber, ActionKey = secondaryAction, TargetEntityId = "humidifier.test" }]
|
||||
}), [new TestAction("curtain-close", "cover.test"), new TestAction(secondaryAction, "humidifier.test")],
|
||||
_clock, NullLogger<BedPresenceAutomation>.Instance, new TestExecutionLog(repository));
|
||||
|
||||
private HealthIb43Service CreateService(Ib43DataRepository repository, BedPresenceAutomation automation)
|
||||
{
|
||||
var options = Options.Create(new Ib43Options { Enabled = true, Devices = [_device] });
|
||||
return new(repository, _context, options, new(options, new(), _clock), _clock, automation);
|
||||
}
|
||||
|
||||
private void User(long id) => _context.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(
|
||||
@@ -166,6 +187,121 @@ public sealed class Ib43PersistenceTests : IDisposable
|
||||
Assert.Equal("Furion.FriendlyException.AppFriendlyException", error.GetType().FullName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Owner_controls_only_the_configured_bed_and_can_read_the_durable_execution_result()
|
||||
{
|
||||
var left = await Assign("left", 1);
|
||||
var right = await Assign("right", 2);
|
||||
await _automation.StartAsync(default);
|
||||
try
|
||||
{
|
||||
User(1);
|
||||
var state = await _service.AutomationStatus(left.Id);
|
||||
Assert.True(state.Available);
|
||||
Assert.False(state.Enabled);
|
||||
Assert.Equal(new[] { "curtain-close", PreviewAction }, state.Rules.Select(rule => rule.ActionKey));
|
||||
await _service.StartAutomation(left.Id);
|
||||
Assert.True((await _service.AutomationStatus(left.Id)).Enabled);
|
||||
await _service.StartAutomation(left.Id, PreviewAction);
|
||||
Assert.All((await _service.AutomationStatus(left.Id)).Rules, rule => Assert.True(rule.Enabled));
|
||||
await ExpectRejection(() => _service.StartAutomation(left.Id, "unknown"));
|
||||
await ExpectRejection(() => _service.StopAutomation(left.Id, "unknown"));
|
||||
|
||||
var id = await _repository.BeginAutomation(left.Id, 1, "curtain-close", "测试窗帘", _clock.Now.ToUnixTimeMilliseconds(), default);
|
||||
Assert.NotNull(id);
|
||||
await _repository.CompleteAutomation(id!, "readback-confirmed", _clock.Now.ToUnixTimeMilliseconds());
|
||||
Assert.Equal("readback-confirmed", Assert.Single((await _service.AutomationStatus(left.Id)).Records).Result);
|
||||
|
||||
User(2);
|
||||
await ExpectRejection(() => _service.AutomationStatus(left.Id));
|
||||
await ExpectRejection(() => _service.StartAutomation(left.Id));
|
||||
await ExpectRejection(() => _service.StopAutomation(left.Id));
|
||||
await ExpectRejection(() => _service.StartAutomation(left.Id, PreviewAction));
|
||||
await ExpectRejection(() => _service.StopAutomation(left.Id, PreviewAction));
|
||||
Assert.False((await _service.AutomationStatus(right.Id)).Available);
|
||||
await ExpectRejection(() => _service.StartAutomation(right.Id));
|
||||
Assert.True(_automation.IsEnabled(left.Id, 1));
|
||||
|
||||
User(1);
|
||||
await _service.StopAutomation(left.Id);
|
||||
Assert.False((await _service.AutomationStatus(left.Id)).Enabled);
|
||||
Assert.True(_automation.IsEnabled(left.Id, 1, PreviewAction));
|
||||
await _service.StartAutomation(left.Id);
|
||||
Admin(); await _service.Release(left.Id);
|
||||
Assert.False(_automation.IsEnabled(left.Id, 1));
|
||||
Assert.False(_automation.IsEnabled(left.Id, 1, PreviewAction));
|
||||
Assert.Null(await _repository.BeginAutomation(left.Id, 1, "curtain-close", "测试窗帘", _clock.Now.ToUnixTimeMilliseconds(), default));
|
||||
User(1);
|
||||
state = await _service.AutomationStatus(left.Id);
|
||||
Assert.False(state.Available);
|
||||
Assert.Single(state.Records);
|
||||
}
|
||||
finally { await _automation.StopAsync(default); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Restarted_backend_services_leave_rules_stopped_and_return_the_same_saved_execution_history()
|
||||
{
|
||||
const string dehumidifier = "dehumidifier-humidity-start";
|
||||
var left = await Assign("left", 1);
|
||||
User(1);
|
||||
Ib43AutomationOutput before;
|
||||
using (var automation = CreateAutomation(_repository, dehumidifier))
|
||||
{
|
||||
await automation.StartAsync(default);
|
||||
try
|
||||
{
|
||||
var service = CreateService(_repository, automation);
|
||||
await service.StartAutomation(left.Id);
|
||||
await service.StartAutomation(left.Id, dehumidifier);
|
||||
foreach (var (action, target) in new[] { ("curtain-close", "测试窗帘"), (dehumidifier, "测试除湿机") })
|
||||
{
|
||||
var now = _clock.GetUtcNow().ToUnixTimeMilliseconds();
|
||||
var id = await _repository.BeginAutomation(left.Id, 1, action, target, now, default);
|
||||
Assert.NotNull(id);
|
||||
await _repository.CompleteAutomation(id!, "readback-confirmed", now);
|
||||
_clock.Now = _clock.Now.AddSeconds(1);
|
||||
}
|
||||
before = await service.AutomationStatus(left.Id);
|
||||
Assert.Equal(2, before.Rules.Count);
|
||||
Assert.All(before.Rules, rule => Assert.True(rule.Enabled));
|
||||
Assert.Equal(2, before.Records.Count);
|
||||
}
|
||||
finally { await automation.StopAsync(default); }
|
||||
}
|
||||
|
||||
_db.Ado.Close();
|
||||
using var reopened = new SqlSugarClient(new ConnectionConfig { ConnectionString = "DataSource=" + _databasePath,
|
||||
DbType = DbType.Sqlite, IsAutoCloseConnection = true, InitKeyType = InitKeyType.Attribute });
|
||||
var repository = new Ib43DataRepository(reopened);
|
||||
using var restarted = CreateAutomation(repository, dehumidifier);
|
||||
await restarted.StartAsync(default);
|
||||
try
|
||||
{
|
||||
var after = await CreateService(repository, restarted).AutomationStatus(left.Id);
|
||||
Assert.False(after.Enabled);
|
||||
Assert.Equal(before.Rules.Select(rule => rule with { Enabled = false }), after.Rules);
|
||||
Assert.Equal(before.Records.ToArray(), after.Records.ToArray());
|
||||
}
|
||||
finally { await restarted.StopAsync(default); }
|
||||
}
|
||||
|
||||
private sealed class TestAction(string key, string target) : IBedPresenceAction
|
||||
{
|
||||
public string Key => key;
|
||||
public string Description => "测试";
|
||||
public bool SupportsTarget(string entityId) => entityId == target;
|
||||
public Task<string> ExecuteAsync(string entityId, CancellationToken token) => throw new InvalidOperationException("No real action in this test");
|
||||
}
|
||||
|
||||
private sealed class TestExecutionLog(Ib43DataRepository repository) : IBedPresenceExecutionLog
|
||||
{
|
||||
public Task<string?> BeginAsync(string bindingId, long userId, string action, string target, DateTimeOffset triggeredAt, CancellationToken token)
|
||||
=> repository.BeginAutomation(bindingId, userId, action, target, triggeredAt.ToUnixTimeMilliseconds(), token);
|
||||
public Task CompleteAsync(string id, string result, DateTimeOffset completedAt)
|
||||
=> repository.CompleteAutomation(id, result, completedAt.ToUnixTimeMilliseconds());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Local_maintenance_previews_then_binds_with_audit_and_is_idempotent()
|
||||
{
|
||||
@@ -201,6 +337,7 @@ public sealed class Ib43PersistenceTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_automation.Dispose();
|
||||
_db.Dispose();
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
File.Delete(_databasePath);
|
||||
|
||||
Reference in New Issue
Block a user