diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/EquipmentInput.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/EquipmentInput.cs
new file mode 100644
index 00000000..bccea960
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/EquipmentInput.cs
@@ -0,0 +1,43 @@
+namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
+
+///
+/// Parameter object carrying the operator-editable fields for an equipment create or update,
+/// so and
+/// avoid an unwieldy positional signature.
+/// The EquipmentId and EquipmentUuid are system-generated (decision #125) and are
+/// therefore not part of this input. Optional string fields that arrive whitespace-only are
+/// collapsed to null by the service.
+///
+/// UNS level-5 segment; matches ^[a-z0-9-]{1,32}$.
+/// Operator colloquial id; unique fleet-wide.
+/// Logical FK to the owning .
+/// Optional driver binding; whitespace/empty means driver-less.
+/// Optional ERP equipment id.
+/// Optional SAP PM equipment id.
+/// Optional OPC 40010 manufacturer name.
+/// Optional OPC 40010 model designation.
+/// Optional OPC 40010 serial number.
+/// Optional OPC 40010 hardware revision.
+/// Optional OPC 40010 software revision.
+/// Optional OPC 40010 year of construction.
+/// Optional OPC 40010 asset location.
+/// Optional OPC 40010 manufacturer URI.
+/// Optional OPC 40010 device-manual URI.
+/// Whether the equipment is surfaced in deployments.
+public sealed record EquipmentInput(
+ string Name,
+ string MachineCode,
+ string UnsLineId,
+ string? DriverInstanceId,
+ string? ZTag,
+ string? SAPID,
+ string? Manufacturer,
+ string? Model,
+ string? SerialNumber,
+ string? HardwareRevision,
+ string? SoftwareRevision,
+ short? YearOfConstruction,
+ string? AssetLocation,
+ string? ManufacturerUri,
+ string? DeviceManualUri,
+ bool Enabled);
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs
index b3ba077b..3d310b7d 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs
@@ -100,4 +100,40 @@ public interface IUnsTreeService
/// A token to cancel the operation.
/// Success, a concurrency failure, or a delete-failed failure.
Task DeleteLineAsync(string unsLineId, byte[] rowVersion, CancellationToken ct = default);
+
+ ///
+ /// Creates a new equipment under a UNS line. The EquipmentId is system-generated
+ /// (decision #125: EQ- + the first 12 hex chars of a fresh EquipmentUuid).
+ /// Fails if the line is unset, if the MachineCode is already used fleet-wide, or if the
+ /// decision-#122 driver-cluster guard trips. Whitespace-only DriverInstanceId/ZTag/SAPID
+ /// collapse to null.
+ ///
+ /// The operator-editable equipment fields.
+ /// A token to cancel the operation.
+ /// Success, a missing-line failure, a duplicate-MachineCode failure, or a #122 guard failure.
+ Task CreateEquipmentAsync(EquipmentInput input, CancellationToken ct = default);
+
+ ///
+ /// Updates an equipment's mutable fields (driver binding, line, name, MachineCode, external
+ /// ids, and the OPC 40010 identification fields). The decision-#122 driver-cluster guard blocks
+ /// binding to a driver in a different cluster than the equipment's line. Uses last-write-wins
+ /// optimistic concurrency on .
+ ///
+ /// The equipment to update.
+ /// The new operator-editable equipment fields.
+ /// The concurrency token the caller last read.
+ /// A token to cancel the operation.
+ /// Success, a missing-row failure, a #122 guard failure, or a concurrency failure.
+ Task UpdateEquipmentAsync(string equipmentId, EquipmentInput input, byte[] rowVersion, CancellationToken ct = default);
+
+ ///
+ /// Deletes an equipment. A missing row is treated as success (already gone). Uses last-write-wins
+ /// optimistic concurrency; a delete that fails because tags or virtual tags still reference the
+ /// equipment surfaces a guidance message.
+ ///
+ /// The equipment to delete.
+ /// The concurrency token the caller last read.
+ /// A token to cancel the operation.
+ /// Success, a concurrency failure, or a delete-failed failure.
+ Task DeleteEquipmentAsync(string equipmentId, byte[] rowVersion, CancellationToken ct = default);
}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs
index ce1bfc82..54406e2c 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs
@@ -336,4 +336,171 @@ public sealed class UnsTreeService(IDbContextFactory dbF
return new UnsMutationResult(false, $"Delete failed: {ex.Message}. Likely because equipment still references this line — remove or re-home it first.");
}
}
+
+ ///
+ public async Task CreateEquipmentAsync(EquipmentInput input, CancellationToken ct = default)
+ {
+ if (string.IsNullOrEmpty(input.UnsLineId))
+ {
+ return new UnsMutationResult(false, "Pick a UNS line.");
+ }
+
+ await using var db = await dbFactory.CreateDbContextAsync(ct);
+
+ if (await db.Equipment.AnyAsync(e => e.MachineCode == input.MachineCode, ct))
+ {
+ return new UnsMutationResult(false, $"MachineCode '{input.MachineCode}' already exists in this fleet.");
+ }
+
+ var guard = await CheckDriverClusterGuardAsync(db, input, ct);
+ if (guard is not null)
+ {
+ return guard.Value;
+ }
+
+ var uuid = Guid.NewGuid();
+ var equipmentId = $"EQ-{uuid.ToString("N")[..12]}";
+
+ db.Equipment.Add(new Equipment
+ {
+ EquipmentId = equipmentId,
+ EquipmentUuid = uuid,
+ DriverInstanceId = string.IsNullOrWhiteSpace(input.DriverInstanceId) ? null : input.DriverInstanceId,
+ UnsLineId = input.UnsLineId,
+ Name = input.Name,
+ MachineCode = input.MachineCode,
+ ZTag = string.IsNullOrWhiteSpace(input.ZTag) ? null : input.ZTag,
+ SAPID = string.IsNullOrWhiteSpace(input.SAPID) ? null : input.SAPID,
+ Manufacturer = input.Manufacturer,
+ Model = input.Model,
+ SerialNumber = input.SerialNumber,
+ HardwareRevision = input.HardwareRevision,
+ SoftwareRevision = input.SoftwareRevision,
+ YearOfConstruction = input.YearOfConstruction,
+ AssetLocation = input.AssetLocation,
+ ManufacturerUri = input.ManufacturerUri,
+ DeviceManualUri = input.DeviceManualUri,
+ Enabled = input.Enabled,
+ });
+ await db.SaveChangesAsync(ct);
+ return new UnsMutationResult(true, null);
+ }
+
+ ///
+ public async Task UpdateEquipmentAsync(
+ string equipmentId,
+ EquipmentInput input,
+ byte[] rowVersion,
+ CancellationToken ct = default)
+ {
+ await using var db = await dbFactory.CreateDbContextAsync(ct);
+
+ var entity = await db.Equipment.FirstOrDefaultAsync(e => e.EquipmentId == equipmentId, ct);
+ if (entity is null)
+ {
+ return new UnsMutationResult(false, "Row no longer exists.");
+ }
+
+ db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
+
+ var guard = await CheckDriverClusterGuardAsync(db, input, ct);
+ if (guard is not null)
+ {
+ return guard.Value;
+ }
+
+ entity.DriverInstanceId = string.IsNullOrWhiteSpace(input.DriverInstanceId) ? null : input.DriverInstanceId;
+ entity.UnsLineId = input.UnsLineId;
+ entity.Name = input.Name;
+ entity.MachineCode = input.MachineCode;
+ entity.ZTag = string.IsNullOrWhiteSpace(input.ZTag) ? null : input.ZTag;
+ entity.SAPID = string.IsNullOrWhiteSpace(input.SAPID) ? null : input.SAPID;
+ entity.Manufacturer = input.Manufacturer;
+ entity.Model = input.Model;
+ entity.SerialNumber = input.SerialNumber;
+ entity.HardwareRevision = input.HardwareRevision;
+ entity.SoftwareRevision = input.SoftwareRevision;
+ entity.YearOfConstruction = input.YearOfConstruction;
+ entity.AssetLocation = input.AssetLocation;
+ entity.ManufacturerUri = input.ManufacturerUri;
+ entity.DeviceManualUri = input.DeviceManualUri;
+ entity.Enabled = input.Enabled;
+
+ try
+ {
+ await db.SaveChangesAsync(ct);
+ return new UnsMutationResult(true, null);
+ }
+ catch (DbUpdateConcurrencyException)
+ {
+ return new UnsMutationResult(false, "Another user changed this equipment while you were editing.");
+ }
+ }
+
+ ///
+ public async Task DeleteEquipmentAsync(
+ string equipmentId,
+ byte[] rowVersion,
+ CancellationToken ct = default)
+ {
+ await using var db = await dbFactory.CreateDbContextAsync(ct);
+
+ var entity = await db.Equipment.FirstOrDefaultAsync(e => e.EquipmentId == equipmentId, ct);
+ if (entity is null)
+ {
+ return new UnsMutationResult(true, null);
+ }
+
+ db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
+ db.Equipment.Remove(entity);
+
+ try
+ {
+ await db.SaveChangesAsync(ct);
+ return new UnsMutationResult(true, null);
+ }
+ catch (DbUpdateConcurrencyException)
+ {
+ return new UnsMutationResult(false, "Another user changed this equipment while you were viewing it.");
+ }
+ catch (Exception ex)
+ {
+ return new UnsMutationResult(false, $"Delete failed: {ex.Message}. Likely because tags or virtual tags reference this equipment — remove them first.");
+ }
+ }
+
+ ///
+ /// Decision #122: an equipment may only bind to a driver in the same cluster as its line.
+ /// Only runs when a driver is requested. Resolves the line's cluster (line → area → cluster)
+ /// and the driver's cluster; when both are known and differ, returns the guard failure.
+ /// Returns null when the bind is allowed (driver-less, unresolvable cluster, or match).
+ ///
+ private static async Task CheckDriverClusterGuardAsync(
+ OtOpcUaConfigDbContext db,
+ EquipmentInput input,
+ CancellationToken ct)
+ {
+ if (string.IsNullOrWhiteSpace(input.DriverInstanceId))
+ {
+ return null;
+ }
+
+ var line = await db.UnsLines.FirstOrDefaultAsync(l => l.UnsLineId == input.UnsLineId, ct);
+ var area = line is null ? null : await db.UnsAreas.FirstOrDefaultAsync(a => a.UnsAreaId == line.UnsAreaId, ct);
+ var lineCluster = area?.ClusterId;
+
+ var driverCluster = await db.DriverInstances
+ .Where(d => d.DriverInstanceId == input.DriverInstanceId)
+ .Select(d => d.ClusterId)
+ .FirstOrDefaultAsync(ct);
+
+ if (driverCluster is not null && lineCluster is not null && driverCluster != lineCluster)
+ {
+ return new UnsMutationResult(
+ false,
+ $"Driver '{input.DriverInstanceId}' is in cluster '{driverCluster}' but the line is in cluster '{lineCluster}' (decision #122).");
+ }
+
+ return null;
+ }
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentTests.cs
new file mode 100644
index 00000000..bb7ee02f
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentTests.cs
@@ -0,0 +1,277 @@
+using Microsoft.EntityFrameworkCore;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
+using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
+
+namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
+
+///
+/// Verifies the Equipment CRUD mutations on , including the
+/// system-generated EQ- id, fleet-wide MachineCode uniqueness, and the decision-#122
+/// driver-cluster guard that blocks binding equipment to a driver in a different cluster than
+/// the equipment's line.
+///
+///
+/// The EF InMemory provider does not enforce RowVersion concurrency, so the
+/// DbUpdateConcurrencyException branches are not exercised here by design.
+///
+[Trait("Category", "Unit")]
+public sealed class UnsTreeServiceEquipmentTests
+{
+ private static (UnsTreeService Service, string DbName) Fresh()
+ {
+ var dbName = $"uns-equip-{Guid.NewGuid():N}";
+ return (new UnsTreeService(UnsTreeTestDb.Factory(dbName)), dbName);
+ }
+
+ ///
+ /// Seeds a line under an area in , plus an optional driver in
+ /// . The line id is always LINE-1; the driver (when
+ /// requested) is always DRV-1.
+ ///
+ private static void SeedLineAndDriver(string dbName, string lineCluster, string? driverCluster)
+ {
+ using var db = UnsTreeTestDb.CreateNamed(dbName);
+ db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-1", ClusterId = lineCluster, Name = "a" });
+ db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-1", UnsAreaId = "AREA-1", Name = "l" });
+ if (driverCluster is not null)
+ {
+ db.DriverInstances.Add(new DriverInstance
+ {
+ DriverInstanceId = "DRV-1",
+ ClusterId = driverCluster,
+ NamespaceId = "NS-1",
+ Name = "drv",
+ DriverType = "ModbusTcp",
+ DriverConfig = "{}",
+ });
+ }
+ db.SaveChanges();
+ }
+
+ private static EquipmentInput Input(string name, string machineCode, string unsLineId, string? driverInstanceId) =>
+ new(name, machineCode, unsLineId, driverInstanceId,
+ ZTag: null, SAPID: null, Manufacturer: null, Model: null, SerialNumber: null,
+ HardwareRevision: null, SoftwareRevision: null, YearOfConstruction: null,
+ AssetLocation: null, ManufacturerUri: null, DeviceManualUri: null, Enabled: true);
+
+ // ----- CreateEquipment -----
+
+ /// A new equipment gets a system-generated EQ- id (EQ- + 12 hex chars) and persists.
+ [Fact]
+ public async Task CreateEquipment_generates_EQ_id_and_persists()
+ {
+ var (service, dbName) = Fresh();
+ SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null);
+
+ var result = await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1", null));
+
+ result.Ok.ShouldBeTrue();
+ result.Error.ShouldBeNull();
+
+ using var db = UnsTreeTestDb.CreateNamed(dbName);
+ var eq = db.Equipment.Single(e => e.MachineCode == "machine_001");
+ eq.EquipmentId.ShouldStartWith("EQ-");
+ eq.EquipmentId.Length.ShouldBe(15); // "EQ-" + 12 hex chars
+ eq.Name.ShouldBe("machine-1");
+ eq.UnsLineId.ShouldBe("LINE-1");
+ eq.Enabled.ShouldBeTrue();
+ }
+
+ /// Creating equipment with a MachineCode already in the fleet is blocked.
+ [Fact]
+ public async Task CreateEquipment_duplicate_machinecode_blocked()
+ {
+ var (service, dbName) = Fresh();
+ SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null);
+ await service.CreateEquipmentAsync(Input("machine-1", "machine_dup", "LINE-1", null));
+
+ var result = await service.CreateEquipmentAsync(Input("machine-2", "machine_dup", "LINE-1", null));
+
+ result.Ok.ShouldBeFalse();
+ result.Error.ShouldBe("MachineCode 'machine_dup' already exists in this fleet.");
+ }
+
+ /// Creating equipment with no UNS line returns the pick-a-line error.
+ [Fact]
+ public async Task CreateEquipment_missing_line_blocked()
+ {
+ var (service, _) = Fresh();
+
+ var result = await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "", null));
+
+ result.Ok.ShouldBeFalse();
+ result.Error.ShouldBe("Pick a UNS line.");
+ }
+
+ /// The #122 guard blocks binding equipment to a driver in a different cluster than the line.
+ [Fact]
+ public async Task CreateEquipment_driver_in_other_cluster_blocked()
+ {
+ var (service, dbName) = Fresh();
+ SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: "SITE-A");
+
+ var result = await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1", "DRV-1"));
+
+ result.Ok.ShouldBeFalse();
+ result.Error.ShouldNotBeNull();
+ result.Error.ShouldContain("decision #122");
+ result.Error.ShouldContain("DRV-1");
+ result.Error.ShouldContain("SITE-A");
+ result.Error.ShouldContain("MAIN");
+
+ using var db = UnsTreeTestDb.CreateNamed(dbName);
+ db.Equipment.Any(e => e.MachineCode == "machine_001").ShouldBeFalse();
+ }
+
+ /// Binding equipment to a driver in the same cluster as the line is allowed.
+ [Fact]
+ public async Task CreateEquipment_driver_in_same_cluster_allowed()
+ {
+ var (service, dbName) = Fresh();
+ SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: "MAIN");
+
+ var result = await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1", "DRV-1"));
+
+ result.Ok.ShouldBeTrue();
+ result.Error.ShouldBeNull();
+
+ using var db = UnsTreeTestDb.CreateNamed(dbName);
+ db.Equipment.Single(e => e.MachineCode == "machine_001").DriverInstanceId.ShouldBe("DRV-1");
+ }
+
+ /// Driver-less equipment is allowed regardless of cluster (no #122 guard applies).
+ [Fact]
+ public async Task CreateEquipment_driverless_allowed()
+ {
+ var (service, dbName) = Fresh();
+ SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null);
+
+ var result = await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1", null));
+
+ result.Ok.ShouldBeTrue();
+ result.Error.ShouldBeNull();
+
+ using var db = UnsTreeTestDb.CreateNamed(dbName);
+ db.Equipment.Single(e => e.MachineCode == "machine_001").DriverInstanceId.ShouldBeNull();
+ }
+
+ // ----- UpdateEquipment -----
+
+ /// Updating equipment changes its mutable fields (name, MachineCode, a 40010 field).
+ [Fact]
+ public async Task UpdateEquipment_changes_fields()
+ {
+ var (service, dbName) = Fresh();
+ SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null);
+ await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1", null));
+
+ string equipmentId;
+ byte[] rv;
+ using (var db = UnsTreeTestDb.CreateNamed(dbName))
+ {
+ var eq = db.Equipment.Single(e => e.MachineCode == "machine_001");
+ equipmentId = eq.EquipmentId;
+ rv = eq.RowVersion;
+ }
+
+ var updated = new EquipmentInput("machine-renamed", "machine_renamed", "LINE-1", null,
+ ZTag: null, SAPID: null, Manufacturer: "Acme", Model: null, SerialNumber: null,
+ HardwareRevision: null, SoftwareRevision: null, YearOfConstruction: (short)2021,
+ AssetLocation: null, ManufacturerUri: null, DeviceManualUri: null, Enabled: false);
+
+ var result = await service.UpdateEquipmentAsync(equipmentId, updated, rv);
+
+ result.Ok.ShouldBeTrue();
+ result.Error.ShouldBeNull();
+
+ using var verify = UnsTreeTestDb.CreateNamed(dbName);
+ var after = verify.Equipment.Single(e => e.EquipmentId == equipmentId);
+ after.Name.ShouldBe("machine-renamed");
+ after.MachineCode.ShouldBe("machine_renamed");
+ after.Manufacturer.ShouldBe("Acme");
+ after.YearOfConstruction.ShouldBe((short)2021);
+ after.Enabled.ShouldBeFalse();
+ }
+
+ /// Updating equipment that no longer exists returns the row-gone error.
+ [Fact]
+ public async Task UpdateEquipment_missing_row_returns_error()
+ {
+ var (service, _) = Fresh();
+
+ var result = await service.UpdateEquipmentAsync("EQ-nope", Input("n", "mc", "LINE-1", null), []);
+
+ result.Ok.ShouldBeFalse();
+ result.Error.ShouldBe("Row no longer exists.");
+ }
+
+ /// The #122 guard blocks an update that binds equipment to a driver in another cluster.
+ [Fact]
+ public async Task UpdateEquipment_driver_in_other_cluster_blocked()
+ {
+ var (service, dbName) = Fresh();
+ SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: "SITE-A");
+ await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1", null));
+
+ string equipmentId;
+ byte[] rv;
+ using (var db = UnsTreeTestDb.CreateNamed(dbName))
+ {
+ var eq = db.Equipment.Single(e => e.MachineCode == "machine_001");
+ equipmentId = eq.EquipmentId;
+ rv = eq.RowVersion;
+ }
+
+ var result = await service.UpdateEquipmentAsync(
+ equipmentId, Input("machine-1", "machine_001", "LINE-1", "DRV-1"), rv);
+
+ result.Ok.ShouldBeFalse();
+ result.Error.ShouldNotBeNull();
+ result.Error.ShouldContain("decision #122");
+
+ using var verify = UnsTreeTestDb.CreateNamed(dbName);
+ verify.Equipment.Single(e => e.EquipmentId == equipmentId).DriverInstanceId.ShouldBeNull();
+ }
+
+ // ----- DeleteEquipment -----
+
+ /// Deleting equipment removes the row.
+ [Fact]
+ public async Task DeleteEquipment_removes_row()
+ {
+ var (service, dbName) = Fresh();
+ SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null);
+ await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1", null));
+
+ string equipmentId;
+ byte[] rv;
+ using (var db = UnsTreeTestDb.CreateNamed(dbName))
+ {
+ var eq = db.Equipment.Single(e => e.MachineCode == "machine_001");
+ equipmentId = eq.EquipmentId;
+ rv = eq.RowVersion;
+ }
+
+ var result = await service.DeleteEquipmentAsync(equipmentId, rv);
+
+ result.Ok.ShouldBeTrue();
+ result.Error.ShouldBeNull();
+
+ using var verify = UnsTreeTestDb.CreateNamed(dbName);
+ verify.Equipment.Any(e => e.EquipmentId == equipmentId).ShouldBeFalse();
+ }
+
+ /// Deleting equipment that is already gone is a no-op success.
+ [Fact]
+ public async Task DeleteEquipment_already_gone_returns_ok()
+ {
+ var (service, _) = Fresh();
+
+ var result = await service.DeleteEquipmentAsync("EQ-ghost", []);
+
+ result.Ok.ShouldBeTrue();
+ result.Error.ShouldBeNull();
+ }
+}