Compare commits
4 Commits
phase-7-st
...
phase-7-fu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63b31e240e | ||
| 78f388b761 | |||
|
|
d78741cfdf | ||
| c08ae0d032 |
@@ -36,6 +36,7 @@
|
|||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Server.Tests/ZB.MOM.WW.OtOpcUa.Server.Tests.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Server.Tests/ZB.MOM.WW.OtOpcUa.Server.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/ZB.MOM.WW.OtOpcUa.Admin.Tests.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/ZB.MOM.WW.OtOpcUa.Admin.Tests.csproj"/>
|
||||||
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Tests.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport.csproj"/>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,232 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Phase 7 follow-up (task #241) — extends <c>dbo.sp_ComputeGenerationDiff</c> to emit
|
||||||
|
/// Script / VirtualTag / ScriptedAlarm rows alongside the existing Namespace /
|
||||||
|
/// DriverInstance / Equipment / Tag / NodeAcl output. Admin DiffViewer now shows
|
||||||
|
/// Phase 7 changes between generations.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Logical ids: ScriptId, VirtualTagId, ScriptedAlarmId — stable across generations
|
||||||
|
/// so a Script whose source changes surfaces as Modified (CHECKSUM picks up the
|
||||||
|
/// SourceHash delta) while a renamed script surfaces as Modified on Name alone.
|
||||||
|
/// ScriptedAlarmState is deliberately excluded — it's not generation-scoped, so
|
||||||
|
/// diffing it between generations is meaningless.
|
||||||
|
/// </remarks>
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ExtendComputeGenerationDiffWithPhase7 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(Procs.ComputeGenerationDiffV3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(Procs.ComputeGenerationDiffV2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Procs
|
||||||
|
{
|
||||||
|
/// <summary>V3 — adds Script / VirtualTag / ScriptedAlarm sections.</summary>
|
||||||
|
public const string ComputeGenerationDiffV3 = @"
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.sp_ComputeGenerationDiff
|
||||||
|
@FromGenerationId bigint,
|
||||||
|
@ToGenerationId bigint
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
CREATE TABLE #diff (TableName nvarchar(32), LogicalId nvarchar(128), ChangeKind nvarchar(16));
|
||||||
|
|
||||||
|
WITH f AS (SELECT NamespaceId AS LogicalId, CHECKSUM(NamespaceUri, Kind, Enabled, Notes) AS Sig FROM dbo.Namespace WHERE GenerationId = @FromGenerationId),
|
||||||
|
t AS (SELECT NamespaceId AS LogicalId, CHECKSUM(NamespaceUri, Kind, Enabled, Notes) AS Sig FROM dbo.Namespace WHERE GenerationId = @ToGenerationId)
|
||||||
|
INSERT #diff
|
||||||
|
SELECT 'Namespace', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)),
|
||||||
|
CASE WHEN f.LogicalId IS NULL THEN 'Added'
|
||||||
|
WHEN t.LogicalId IS NULL THEN 'Removed'
|
||||||
|
WHEN f.Sig <> t.Sig THEN 'Modified'
|
||||||
|
ELSE 'Unchanged' END
|
||||||
|
FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId
|
||||||
|
WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig;
|
||||||
|
|
||||||
|
WITH f AS (SELECT DriverInstanceId AS LogicalId, CHECKSUM(ClusterId, NamespaceId, Name, DriverType, Enabled, CONVERT(varchar(max), DriverConfig)) AS Sig FROM dbo.DriverInstance WHERE GenerationId = @FromGenerationId),
|
||||||
|
t AS (SELECT DriverInstanceId AS LogicalId, CHECKSUM(ClusterId, NamespaceId, Name, DriverType, Enabled, CONVERT(varchar(max), DriverConfig)) AS Sig FROM dbo.DriverInstance WHERE GenerationId = @ToGenerationId)
|
||||||
|
INSERT #diff
|
||||||
|
SELECT 'DriverInstance', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)),
|
||||||
|
CASE WHEN f.LogicalId IS NULL THEN 'Added'
|
||||||
|
WHEN t.LogicalId IS NULL THEN 'Removed'
|
||||||
|
WHEN f.Sig <> t.Sig THEN 'Modified'
|
||||||
|
ELSE 'Unchanged' END
|
||||||
|
FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId
|
||||||
|
WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig;
|
||||||
|
|
||||||
|
WITH f AS (SELECT EquipmentId AS LogicalId, CHECKSUM(EquipmentUuid, DriverInstanceId, UnsLineId, Name, MachineCode, ZTag, SAPID, EquipmentClassRef, Manufacturer, Model, SerialNumber) AS Sig FROM dbo.Equipment WHERE GenerationId = @FromGenerationId),
|
||||||
|
t AS (SELECT EquipmentId AS LogicalId, CHECKSUM(EquipmentUuid, DriverInstanceId, UnsLineId, Name, MachineCode, ZTag, SAPID, EquipmentClassRef, Manufacturer, Model, SerialNumber) AS Sig FROM dbo.Equipment WHERE GenerationId = @ToGenerationId)
|
||||||
|
INSERT #diff
|
||||||
|
SELECT 'Equipment', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)),
|
||||||
|
CASE WHEN f.LogicalId IS NULL THEN 'Added'
|
||||||
|
WHEN t.LogicalId IS NULL THEN 'Removed'
|
||||||
|
WHEN f.Sig <> t.Sig THEN 'Modified'
|
||||||
|
ELSE 'Unchanged' END
|
||||||
|
FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId
|
||||||
|
WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig;
|
||||||
|
|
||||||
|
WITH f AS (SELECT TagId AS LogicalId, CHECKSUM(DriverInstanceId, DeviceId, EquipmentId, PollGroupId, FolderPath, Name, DataType, AccessLevel, WriteIdempotent, CONVERT(varchar(max), TagConfig)) AS Sig FROM dbo.Tag WHERE GenerationId = @FromGenerationId),
|
||||||
|
t AS (SELECT TagId AS LogicalId, CHECKSUM(DriverInstanceId, DeviceId, EquipmentId, PollGroupId, FolderPath, Name, DataType, AccessLevel, WriteIdempotent, CONVERT(varchar(max), TagConfig)) AS Sig FROM dbo.Tag WHERE GenerationId = @ToGenerationId)
|
||||||
|
INSERT #diff
|
||||||
|
SELECT 'Tag', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)),
|
||||||
|
CASE WHEN f.LogicalId IS NULL THEN 'Added'
|
||||||
|
WHEN t.LogicalId IS NULL THEN 'Removed'
|
||||||
|
WHEN f.Sig <> t.Sig THEN 'Modified'
|
||||||
|
ELSE 'Unchanged' END
|
||||||
|
FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId
|
||||||
|
WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig;
|
||||||
|
|
||||||
|
WITH f AS (
|
||||||
|
SELECT CONVERT(nvarchar(128), LdapGroup + '|' + CONVERT(nvarchar(16), ScopeKind) + '|' + ISNULL(ScopeId, '(cluster)')) AS LogicalId,
|
||||||
|
CHECKSUM(ClusterId, PermissionFlags, Notes) AS Sig
|
||||||
|
FROM dbo.NodeAcl WHERE GenerationId = @FromGenerationId),
|
||||||
|
t AS (
|
||||||
|
SELECT CONVERT(nvarchar(128), LdapGroup + '|' + CONVERT(nvarchar(16), ScopeKind) + '|' + ISNULL(ScopeId, '(cluster)')) AS LogicalId,
|
||||||
|
CHECKSUM(ClusterId, PermissionFlags, Notes) AS Sig
|
||||||
|
FROM dbo.NodeAcl WHERE GenerationId = @ToGenerationId)
|
||||||
|
INSERT #diff
|
||||||
|
SELECT 'NodeAcl', COALESCE(f.LogicalId, t.LogicalId),
|
||||||
|
CASE WHEN f.LogicalId IS NULL THEN 'Added'
|
||||||
|
WHEN t.LogicalId IS NULL THEN 'Removed'
|
||||||
|
WHEN f.Sig <> t.Sig THEN 'Modified'
|
||||||
|
ELSE 'Unchanged' END
|
||||||
|
FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId
|
||||||
|
WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig;
|
||||||
|
|
||||||
|
-- Phase 7 — Script section. CHECKSUM picks up source changes via SourceHash + rename
|
||||||
|
-- via Name; Language future-proofs for non-C# engines. Same Name + same Source =
|
||||||
|
-- Unchanged (identical hash).
|
||||||
|
WITH f AS (SELECT ScriptId AS LogicalId, CHECKSUM(Name, SourceHash, Language) AS Sig FROM dbo.Script WHERE GenerationId = @FromGenerationId),
|
||||||
|
t AS (SELECT ScriptId AS LogicalId, CHECKSUM(Name, SourceHash, Language) AS Sig FROM dbo.Script WHERE GenerationId = @ToGenerationId)
|
||||||
|
INSERT #diff
|
||||||
|
SELECT 'Script', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)),
|
||||||
|
CASE WHEN f.LogicalId IS NULL THEN 'Added'
|
||||||
|
WHEN t.LogicalId IS NULL THEN 'Removed'
|
||||||
|
WHEN f.Sig <> t.Sig THEN 'Modified'
|
||||||
|
ELSE 'Unchanged' END
|
||||||
|
FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId
|
||||||
|
WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig;
|
||||||
|
|
||||||
|
-- Phase 7 — VirtualTag section.
|
||||||
|
WITH f AS (SELECT VirtualTagId AS LogicalId, CHECKSUM(EquipmentId, Name, DataType, ScriptId, ChangeTriggered, TimerIntervalMs, Historize, Enabled) AS Sig FROM dbo.VirtualTag WHERE GenerationId = @FromGenerationId),
|
||||||
|
t AS (SELECT VirtualTagId AS LogicalId, CHECKSUM(EquipmentId, Name, DataType, ScriptId, ChangeTriggered, TimerIntervalMs, Historize, Enabled) AS Sig FROM dbo.VirtualTag WHERE GenerationId = @ToGenerationId)
|
||||||
|
INSERT #diff
|
||||||
|
SELECT 'VirtualTag', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)),
|
||||||
|
CASE WHEN f.LogicalId IS NULL THEN 'Added'
|
||||||
|
WHEN t.LogicalId IS NULL THEN 'Removed'
|
||||||
|
WHEN f.Sig <> t.Sig THEN 'Modified'
|
||||||
|
ELSE 'Unchanged' END
|
||||||
|
FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId
|
||||||
|
WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig;
|
||||||
|
|
||||||
|
-- Phase 7 — ScriptedAlarm section. ScriptedAlarmState (operator ack trail) is
|
||||||
|
-- logical-id keyed outside the generation scope + intentionally excluded here —
|
||||||
|
-- diffing ack state between generations is semantically meaningless.
|
||||||
|
WITH f AS (SELECT ScriptedAlarmId AS LogicalId, CHECKSUM(EquipmentId, Name, AlarmType, Severity, MessageTemplate, PredicateScriptId, HistorizeToAveva, Retain, Enabled) AS Sig FROM dbo.ScriptedAlarm WHERE GenerationId = @FromGenerationId),
|
||||||
|
t AS (SELECT ScriptedAlarmId AS LogicalId, CHECKSUM(EquipmentId, Name, AlarmType, Severity, MessageTemplate, PredicateScriptId, HistorizeToAveva, Retain, Enabled) AS Sig FROM dbo.ScriptedAlarm WHERE GenerationId = @ToGenerationId)
|
||||||
|
INSERT #diff
|
||||||
|
SELECT 'ScriptedAlarm', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)),
|
||||||
|
CASE WHEN f.LogicalId IS NULL THEN 'Added'
|
||||||
|
WHEN t.LogicalId IS NULL THEN 'Removed'
|
||||||
|
WHEN f.Sig <> t.Sig THEN 'Modified'
|
||||||
|
ELSE 'Unchanged' END
|
||||||
|
FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId
|
||||||
|
WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig;
|
||||||
|
|
||||||
|
SELECT TableName, LogicalId, ChangeKind FROM #diff;
|
||||||
|
DROP TABLE #diff;
|
||||||
|
END
|
||||||
|
";
|
||||||
|
|
||||||
|
/// <summary>V2 — restores the pre-Phase-7 proc on Down().</summary>
|
||||||
|
public const string ComputeGenerationDiffV2 = @"
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.sp_ComputeGenerationDiff
|
||||||
|
@FromGenerationId bigint,
|
||||||
|
@ToGenerationId bigint
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
CREATE TABLE #diff (TableName nvarchar(32), LogicalId nvarchar(128), ChangeKind nvarchar(16));
|
||||||
|
|
||||||
|
WITH f AS (SELECT NamespaceId AS LogicalId, CHECKSUM(NamespaceUri, Kind, Enabled, Notes) AS Sig FROM dbo.Namespace WHERE GenerationId = @FromGenerationId),
|
||||||
|
t AS (SELECT NamespaceId AS LogicalId, CHECKSUM(NamespaceUri, Kind, Enabled, Notes) AS Sig FROM dbo.Namespace WHERE GenerationId = @ToGenerationId)
|
||||||
|
INSERT #diff
|
||||||
|
SELECT 'Namespace', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)),
|
||||||
|
CASE WHEN f.LogicalId IS NULL THEN 'Added'
|
||||||
|
WHEN t.LogicalId IS NULL THEN 'Removed'
|
||||||
|
WHEN f.Sig <> t.Sig THEN 'Modified'
|
||||||
|
ELSE 'Unchanged' END
|
||||||
|
FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId
|
||||||
|
WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig;
|
||||||
|
|
||||||
|
WITH f AS (SELECT DriverInstanceId AS LogicalId, CHECKSUM(ClusterId, NamespaceId, Name, DriverType, Enabled, CONVERT(varchar(max), DriverConfig)) AS Sig FROM dbo.DriverInstance WHERE GenerationId = @FromGenerationId),
|
||||||
|
t AS (SELECT DriverInstanceId AS LogicalId, CHECKSUM(ClusterId, NamespaceId, Name, DriverType, Enabled, CONVERT(varchar(max), DriverConfig)) AS Sig FROM dbo.DriverInstance WHERE GenerationId = @ToGenerationId)
|
||||||
|
INSERT #diff
|
||||||
|
SELECT 'DriverInstance', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)),
|
||||||
|
CASE WHEN f.LogicalId IS NULL THEN 'Added'
|
||||||
|
WHEN t.LogicalId IS NULL THEN 'Removed'
|
||||||
|
WHEN f.Sig <> t.Sig THEN 'Modified'
|
||||||
|
ELSE 'Unchanged' END
|
||||||
|
FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId
|
||||||
|
WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig;
|
||||||
|
|
||||||
|
WITH f AS (SELECT EquipmentId AS LogicalId, CHECKSUM(EquipmentUuid, DriverInstanceId, UnsLineId, Name, MachineCode, ZTag, SAPID, EquipmentClassRef, Manufacturer, Model, SerialNumber) AS Sig FROM dbo.Equipment WHERE GenerationId = @FromGenerationId),
|
||||||
|
t AS (SELECT EquipmentId AS LogicalId, CHECKSUM(EquipmentUuid, DriverInstanceId, UnsLineId, Name, MachineCode, ZTag, SAPID, EquipmentClassRef, Manufacturer, Model, SerialNumber) AS Sig FROM dbo.Equipment WHERE GenerationId = @ToGenerationId)
|
||||||
|
INSERT #diff
|
||||||
|
SELECT 'Equipment', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)),
|
||||||
|
CASE WHEN f.LogicalId IS NULL THEN 'Added'
|
||||||
|
WHEN t.LogicalId IS NULL THEN 'Removed'
|
||||||
|
WHEN f.Sig <> t.Sig THEN 'Modified'
|
||||||
|
ELSE 'Unchanged' END
|
||||||
|
FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId
|
||||||
|
WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig;
|
||||||
|
|
||||||
|
WITH f AS (SELECT TagId AS LogicalId, CHECKSUM(DriverInstanceId, DeviceId, EquipmentId, PollGroupId, FolderPath, Name, DataType, AccessLevel, WriteIdempotent, CONVERT(varchar(max), TagConfig)) AS Sig FROM dbo.Tag WHERE GenerationId = @FromGenerationId),
|
||||||
|
t AS (SELECT TagId AS LogicalId, CHECKSUM(DriverInstanceId, DeviceId, EquipmentId, PollGroupId, FolderPath, Name, DataType, AccessLevel, WriteIdempotent, CONVERT(varchar(max), TagConfig)) AS Sig FROM dbo.Tag WHERE GenerationId = @ToGenerationId)
|
||||||
|
INSERT #diff
|
||||||
|
SELECT 'Tag', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)),
|
||||||
|
CASE WHEN f.LogicalId IS NULL THEN 'Added'
|
||||||
|
WHEN t.LogicalId IS NULL THEN 'Removed'
|
||||||
|
WHEN f.Sig <> t.Sig THEN 'Modified'
|
||||||
|
ELSE 'Unchanged' END
|
||||||
|
FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId
|
||||||
|
WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig;
|
||||||
|
|
||||||
|
WITH f AS (
|
||||||
|
SELECT CONVERT(nvarchar(128), LdapGroup + '|' + CONVERT(nvarchar(16), ScopeKind) + '|' + ISNULL(ScopeId, '(cluster)')) AS LogicalId,
|
||||||
|
CHECKSUM(ClusterId, PermissionFlags, Notes) AS Sig
|
||||||
|
FROM dbo.NodeAcl WHERE GenerationId = @FromGenerationId),
|
||||||
|
t AS (
|
||||||
|
SELECT CONVERT(nvarchar(128), LdapGroup + '|' + CONVERT(nvarchar(16), ScopeKind) + '|' + ISNULL(ScopeId, '(cluster)')) AS LogicalId,
|
||||||
|
CHECKSUM(ClusterId, PermissionFlags, Notes) AS Sig
|
||||||
|
FROM dbo.NodeAcl WHERE GenerationId = @ToGenerationId)
|
||||||
|
INSERT #diff
|
||||||
|
SELECT 'NodeAcl', COALESCE(f.LogicalId, t.LogicalId),
|
||||||
|
CASE WHEN f.LogicalId IS NULL THEN 'Added'
|
||||||
|
WHEN t.LogicalId IS NULL THEN 'Removed'
|
||||||
|
WHEN f.Sig <> t.Sig THEN 'Modified'
|
||||||
|
ELSE 'Unchanged' END
|
||||||
|
FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId
|
||||||
|
WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig;
|
||||||
|
|
||||||
|
SELECT TableName, LogicalId, ChangeKind FROM #diff;
|
||||||
|
DROP TABLE #diff;
|
||||||
|
END
|
||||||
|
";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,14 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
|||||||
private readonly Func<string, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverTier>? _tierLookup;
|
private readonly Func<string, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverTier>? _tierLookup;
|
||||||
private readonly Func<string, string?>? _resilienceConfigLookup;
|
private readonly Func<string, string?>? _resilienceConfigLookup;
|
||||||
private readonly Func<string, ZB.MOM.WW.OtOpcUa.Core.OpcUa.EquipmentNamespaceContent?>? _equipmentContentLookup;
|
private readonly Func<string, ZB.MOM.WW.OtOpcUa.Core.OpcUa.EquipmentNamespaceContent?>? _equipmentContentLookup;
|
||||||
|
|
||||||
|
// Phase 7 Stream G follow-up (task #239). When composed with the VirtualTagEngine +
|
||||||
|
// ScriptedAlarmEngine sources these route node reads to the engines instead of the
|
||||||
|
// driver. Null = Phase 7 engines not enabled for this deployment (identical to pre-
|
||||||
|
// Phase-7 behaviour).
|
||||||
|
private readonly ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? _virtualReadable;
|
||||||
|
private readonly ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? _scriptedAlarmReadable;
|
||||||
|
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly ILogger<OpcUaApplicationHost> _logger;
|
private readonly ILogger<OpcUaApplicationHost> _logger;
|
||||||
private ApplicationInstance? _application;
|
private ApplicationInstance? _application;
|
||||||
@@ -45,7 +53,9 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
|||||||
StaleConfigFlag? staleConfigFlag = null,
|
StaleConfigFlag? staleConfigFlag = null,
|
||||||
Func<string, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverTier>? tierLookup = null,
|
Func<string, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverTier>? tierLookup = null,
|
||||||
Func<string, string?>? resilienceConfigLookup = null,
|
Func<string, string?>? resilienceConfigLookup = null,
|
||||||
Func<string, ZB.MOM.WW.OtOpcUa.Core.OpcUa.EquipmentNamespaceContent?>? equipmentContentLookup = null)
|
Func<string, ZB.MOM.WW.OtOpcUa.Core.OpcUa.EquipmentNamespaceContent?>? equipmentContentLookup = null,
|
||||||
|
ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? virtualReadable = null,
|
||||||
|
ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? scriptedAlarmReadable = null)
|
||||||
{
|
{
|
||||||
_options = options;
|
_options = options;
|
||||||
_driverHost = driverHost;
|
_driverHost = driverHost;
|
||||||
@@ -57,6 +67,8 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
|||||||
_tierLookup = tierLookup;
|
_tierLookup = tierLookup;
|
||||||
_resilienceConfigLookup = resilienceConfigLookup;
|
_resilienceConfigLookup = resilienceConfigLookup;
|
||||||
_equipmentContentLookup = equipmentContentLookup;
|
_equipmentContentLookup = equipmentContentLookup;
|
||||||
|
_virtualReadable = virtualReadable;
|
||||||
|
_scriptedAlarmReadable = scriptedAlarmReadable;
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@@ -85,7 +97,8 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
|||||||
|
|
||||||
_server = new OtOpcUaServer(_driverHost, _authenticator, _pipelineBuilder, _loggerFactory,
|
_server = new OtOpcUaServer(_driverHost, _authenticator, _pipelineBuilder, _loggerFactory,
|
||||||
authzGate: _authzGate, scopeResolver: _scopeResolver,
|
authzGate: _authzGate, scopeResolver: _scopeResolver,
|
||||||
tierLookup: _tierLookup, resilienceConfigLookup: _resilienceConfigLookup);
|
tierLookup: _tierLookup, resilienceConfigLookup: _resilienceConfigLookup,
|
||||||
|
virtualReadable: _virtualReadable, scriptedAlarmReadable: _scriptedAlarmReadable);
|
||||||
await _application.Start(_server).ConfigureAwait(false);
|
await _application.Start(_server).ConfigureAwait(false);
|
||||||
|
|
||||||
_logger.LogInformation("OPC UA server started — endpoint={Endpoint} driverCount={Count}",
|
_logger.LogInformation("OPC UA server started — endpoint={Endpoint} driverCount={Count}",
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ public sealed class OtOpcUaServer : StandardServer
|
|||||||
private readonly NodeScopeResolver? _scopeResolver;
|
private readonly NodeScopeResolver? _scopeResolver;
|
||||||
private readonly Func<string, DriverTier>? _tierLookup;
|
private readonly Func<string, DriverTier>? _tierLookup;
|
||||||
private readonly Func<string, string?>? _resilienceConfigLookup;
|
private readonly Func<string, string?>? _resilienceConfigLookup;
|
||||||
|
|
||||||
|
// Phase 7 Stream G follow-up wiring (task #239). Shared across every DriverNodeManager
|
||||||
|
// instantiated by this server so virtual-tag reads and scripted-alarm reads from any
|
||||||
|
// driver's address-space subtree route to the same engine. When null (no Phase 7
|
||||||
|
// engines composed for this deployment) DriverNodeManager falls back to driver-only
|
||||||
|
// dispatch — identical to pre-Phase-7 behaviour.
|
||||||
|
private readonly IReadable? _virtualReadable;
|
||||||
|
private readonly IReadable? _scriptedAlarmReadable;
|
||||||
|
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly List<DriverNodeManager> _driverNodeManagers = new();
|
private readonly List<DriverNodeManager> _driverNodeManagers = new();
|
||||||
|
|
||||||
@@ -36,7 +45,9 @@ public sealed class OtOpcUaServer : StandardServer
|
|||||||
AuthorizationGate? authzGate = null,
|
AuthorizationGate? authzGate = null,
|
||||||
NodeScopeResolver? scopeResolver = null,
|
NodeScopeResolver? scopeResolver = null,
|
||||||
Func<string, DriverTier>? tierLookup = null,
|
Func<string, DriverTier>? tierLookup = null,
|
||||||
Func<string, string?>? resilienceConfigLookup = null)
|
Func<string, string?>? resilienceConfigLookup = null,
|
||||||
|
IReadable? virtualReadable = null,
|
||||||
|
IReadable? scriptedAlarmReadable = null)
|
||||||
{
|
{
|
||||||
_driverHost = driverHost;
|
_driverHost = driverHost;
|
||||||
_authenticator = authenticator;
|
_authenticator = authenticator;
|
||||||
@@ -45,6 +56,8 @@ public sealed class OtOpcUaServer : StandardServer
|
|||||||
_scopeResolver = scopeResolver;
|
_scopeResolver = scopeResolver;
|
||||||
_tierLookup = tierLookup;
|
_tierLookup = tierLookup;
|
||||||
_resilienceConfigLookup = resilienceConfigLookup;
|
_resilienceConfigLookup = resilienceConfigLookup;
|
||||||
|
_virtualReadable = virtualReadable;
|
||||||
|
_scriptedAlarmReadable = scriptedAlarmReadable;
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +90,8 @@ public sealed class OtOpcUaServer : StandardServer
|
|||||||
|
|
||||||
var invoker = new CapabilityInvoker(_pipelineBuilder, driver.DriverInstanceId, () => options, driver.DriverType);
|
var invoker = new CapabilityInvoker(_pipelineBuilder, driver.DriverInstanceId, () => options, driver.DriverType);
|
||||||
var manager = new DriverNodeManager(server, configuration, driver, invoker, logger,
|
var manager = new DriverNodeManager(server, configuration, driver, invoker, logger,
|
||||||
authzGate: _authzGate, scopeResolver: _scopeResolver);
|
authzGate: _authzGate, scopeResolver: _scopeResolver,
|
||||||
|
virtualReadable: _virtualReadable, scriptedAlarmReadable: _scriptedAlarmReadable);
|
||||||
_driverNodeManagers.Add(manager);
|
_driverNodeManagers.Add(manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
130
tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/AdminWebAppFactory.cs
Normal file
130
tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/AdminWebAppFactory.cs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Admin.E2ETests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stands up the Admin Blazor Server host on a free TCP port with the live SQL Server
|
||||||
|
/// context swapped for an EF Core InMemory DbContext + the LDAP cookie auth swapped for
|
||||||
|
/// <see cref="TestAuthHandler"/>. Playwright connects to <see cref="BaseUrl"/>.
|
||||||
|
/// InMemory is sufficient because UnsService's drag-drop path exercises EF operations,
|
||||||
|
/// not raw SQL.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// We deliberately build a <see cref="WebApplication"/> directly rather than going through
|
||||||
|
/// <c>WebApplicationFactory<Program></c> — the factory's TestServer transport doesn't
|
||||||
|
/// coexist cleanly with Kestrel-on-a-real-port, and Playwright needs a real loopback HTTP
|
||||||
|
/// endpoint to hit. This mirrors the Program.cs entry-points for everything else.
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class AdminWebAppFactory : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private WebApplication? _app;
|
||||||
|
|
||||||
|
public string BaseUrl { get; private set; } = "";
|
||||||
|
public long SeededGenerationId { get; private set; }
|
||||||
|
public string SeededClusterId { get; } = "e2e-cluster";
|
||||||
|
|
||||||
|
public async Task StartAsync()
|
||||||
|
{
|
||||||
|
var port = GetFreeTcpPort();
|
||||||
|
BaseUrl = $"http://127.0.0.1:{port}";
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(Array.Empty<string>());
|
||||||
|
builder.WebHost.UseUrls(BaseUrl);
|
||||||
|
|
||||||
|
// --- Mirror the Admin composition in Program.cs, but with the InMemory DB + test
|
||||||
|
// auth swaps instead of SQL Server + LDAP cookie auth.
|
||||||
|
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
builder.Services.AddSignalR();
|
||||||
|
builder.Services.AddAntiforgery();
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication(TestAuthHandler.SchemeName)
|
||||||
|
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(TestAuthHandler.SchemeName, _ => { });
|
||||||
|
builder.Services.AddAuthorizationBuilder()
|
||||||
|
.AddPolicy("CanEdit", p => p.RequireRole(Admin.Services.AdminRoles.ConfigEditor, Admin.Services.AdminRoles.FleetAdmin))
|
||||||
|
.AddPolicy("CanPublish", p => p.RequireRole(Admin.Services.AdminRoles.FleetAdmin));
|
||||||
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<OtOpcUaConfigDbContext>(opt =>
|
||||||
|
opt.UseInMemoryDatabase($"e2e-{Guid.NewGuid():N}"));
|
||||||
|
|
||||||
|
builder.Services.AddScoped<Admin.Services.ClusterService>();
|
||||||
|
builder.Services.AddScoped<Admin.Services.GenerationService>();
|
||||||
|
builder.Services.AddScoped<Admin.Services.UnsService>();
|
||||||
|
builder.Services.AddScoped<Admin.Services.EquipmentService>();
|
||||||
|
builder.Services.AddScoped<Admin.Services.NamespaceService>();
|
||||||
|
builder.Services.AddScoped<Admin.Services.DriverInstanceService>();
|
||||||
|
builder.Services.AddScoped<Admin.Services.DraftValidationService>();
|
||||||
|
|
||||||
|
_app = builder.Build();
|
||||||
|
_app.UseStaticFiles();
|
||||||
|
_app.UseRouting();
|
||||||
|
_app.UseAuthentication();
|
||||||
|
_app.UseAuthorization();
|
||||||
|
_app.UseAntiforgery();
|
||||||
|
_app.MapRazorComponents<Admin.Components.App>().AddInteractiveServerRenderMode();
|
||||||
|
|
||||||
|
// Seed the draft BEFORE starting the host so Playwright sees a ready page on first nav.
|
||||||
|
using (var scope = _app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<OtOpcUaConfigDbContext>();
|
||||||
|
SeededGenerationId = Seed(db, SeededClusterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _app.StartAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_app is not null)
|
||||||
|
{
|
||||||
|
await _app.StopAsync();
|
||||||
|
await _app.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long Seed(OtOpcUaConfigDbContext db, string clusterId)
|
||||||
|
{
|
||||||
|
var cluster = new ServerCluster
|
||||||
|
{
|
||||||
|
ClusterId = clusterId, Name = "e2e", Enterprise = "zb", Site = "lab",
|
||||||
|
RedundancyMode = RedundancyMode.None, NodeCount = 1, CreatedBy = "e2e",
|
||||||
|
};
|
||||||
|
var gen = new ConfigGeneration
|
||||||
|
{
|
||||||
|
ClusterId = clusterId, Status = GenerationStatus.Draft, CreatedBy = "e2e",
|
||||||
|
};
|
||||||
|
|
||||||
|
db.ServerClusters.Add(cluster);
|
||||||
|
db.ConfigGenerations.Add(gen);
|
||||||
|
db.SaveChanges();
|
||||||
|
|
||||||
|
db.UnsAreas.AddRange(
|
||||||
|
new UnsArea { UnsAreaId = "area-a", ClusterId = clusterId, Name = "warsaw", GenerationId = gen.GenerationId },
|
||||||
|
new UnsArea { UnsAreaId = "area-b", ClusterId = clusterId, Name = "berlin", GenerationId = gen.GenerationId });
|
||||||
|
db.UnsLines.Add(new UnsLine
|
||||||
|
{
|
||||||
|
UnsLineId = "line-a1", UnsAreaId = "area-a", Name = "oven-line", GenerationId = gen.GenerationId,
|
||||||
|
});
|
||||||
|
db.SaveChanges();
|
||||||
|
return gen.GenerationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetFreeTcpPort()
|
||||||
|
{
|
||||||
|
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
|
listener.Start();
|
||||||
|
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||||
|
listener.Stop();
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/PlaywrightFixture.cs
Normal file
44
tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/PlaywrightFixture.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using Microsoft.Playwright;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Admin.E2ETests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One Playwright runtime + Chromium browser for the whole E2E suite. Tests
|
||||||
|
/// open a fresh <see cref="IBrowserContext"/> per fixture so cookies + localStorage
|
||||||
|
/// stay isolated. Browser install is a one-time step:
|
||||||
|
/// <c>pwsh tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/bin/Debug/net10.0/playwright.ps1 install chromium</c>.
|
||||||
|
/// When the browser binary isn't present the suite reports a <see cref="PlaywrightBrowserMissingException"/>
|
||||||
|
/// so CI can distinguish missing-browser from real test failure.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PlaywrightFixture : IAsyncLifetime
|
||||||
|
{
|
||||||
|
public IPlaywright Playwright { get; private set; } = null!;
|
||||||
|
public IBrowser Browser { get; private set; } = null!;
|
||||||
|
|
||||||
|
public async ValueTask InitializeAsync()
|
||||||
|
{
|
||||||
|
Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Browser = await Playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true });
|
||||||
|
}
|
||||||
|
catch (PlaywrightException ex) when (ex.Message.Contains("Executable doesn't exist"))
|
||||||
|
{
|
||||||
|
throw new PlaywrightBrowserMissingException(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (Browser is not null) await Browser.CloseAsync();
|
||||||
|
Playwright?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown by <see cref="PlaywrightFixture"/> when Chromium isn't installed. Tests
|
||||||
|
/// catching this mark themselves as "skipped" rather than "failed", so CI without
|
||||||
|
/// the install step stays green.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PlaywrightBrowserMissingException(string message) : Exception(message);
|
||||||
34
tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/TestAuthHandler.cs
Normal file
34
tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/TestAuthHandler.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Admin.Services;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Admin.E2ETests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stamps every request with a FleetAdmin principal so E2E tests can hit
|
||||||
|
/// authenticated Razor pages without the LDAP login flow. Registered as the
|
||||||
|
/// default authentication scheme by <see cref="AdminWebAppFactory"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TestAuthHandler(
|
||||||
|
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||||
|
ILoggerFactory logger,
|
||||||
|
UrlEncoder encoder)
|
||||||
|
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
|
||||||
|
{
|
||||||
|
public const string SchemeName = "Test";
|
||||||
|
|
||||||
|
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
|
{
|
||||||
|
var claims = new[]
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.Name, "e2e-test-user"),
|
||||||
|
new Claim(ClaimTypes.Role, AdminRoles.FleetAdmin),
|
||||||
|
};
|
||||||
|
var identity = new ClaimsIdentity(claims, SchemeName);
|
||||||
|
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), SchemeName);
|
||||||
|
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using Microsoft.Playwright;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Admin.E2ETests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase 6.4 UnsTab drag-drop E2E smoke (task #199). This PR lands the Playwright +
|
||||||
|
/// WebApplicationFactory-equivalent scaffolding so future E2E coverage builds on it
|
||||||
|
/// rather than setting it up from scratch.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <b>Prerequisite.</b> Chromium must be installed locally:
|
||||||
|
/// <c>pwsh tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/bin/Debug/net10.0/playwright.ps1 install chromium</c>.
|
||||||
|
/// When the binary is missing the tests <see cref="Assert.Skip"/> rather than fail hard,
|
||||||
|
/// so CI pipelines that don't run the install step still report green.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <b>Current scope.</b> The host-reachability smoke below proves the infra works:
|
||||||
|
/// Kestrel-on-a-free-port, InMemory DbContext swap, <see cref="TestAuthHandler"/>
|
||||||
|
/// bypass, and Playwright-to-real-browser are all exercised. The actual drag-drop
|
||||||
|
/// interactive assertion is filed as a follow-up (task #242) because
|
||||||
|
/// Blazor Server interactive render through a test-owned pipeline needs a dedicated
|
||||||
|
/// diagnosis pass — the scaffolding lands here first so that follow-up can focus on
|
||||||
|
/// the Blazor-specific wiring instead of rebuilding the harness.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
[Trait("Category", "E2E")]
|
||||||
|
public sealed class UnsTabDragDropE2ETests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Admin_host_serves_HTTP_via_Playwright_scaffolding()
|
||||||
|
{
|
||||||
|
await using var app = new AdminWebAppFactory();
|
||||||
|
await app.StartAsync();
|
||||||
|
|
||||||
|
PlaywrightFixture fixture;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fixture = new PlaywrightFixture();
|
||||||
|
await fixture.InitializeAsync();
|
||||||
|
}
|
||||||
|
catch (PlaywrightBrowserMissingException)
|
||||||
|
{
|
||||||
|
Assert.Skip("Chromium not installed. Run playwright.ps1 install chromium.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ctx = await fixture.Browser.NewContextAsync();
|
||||||
|
var page = await ctx.NewPageAsync();
|
||||||
|
|
||||||
|
// Navigate to the root. We only assert the host is live + returns HTML — not
|
||||||
|
// that the Blazor Server interactive render has booted. Booting the interactive
|
||||||
|
// circuit in a test-owned pipeline is task #242.
|
||||||
|
var response = await page.GotoAsync(app.BaseUrl);
|
||||||
|
|
||||||
|
response.ShouldNotBeNull();
|
||||||
|
response!.Status.ShouldBeLessThan(500,
|
||||||
|
$"Admin host returned HTTP {response.Status} at root — scaffolding broken");
|
||||||
|
|
||||||
|
// Static HTML shell should at least include the <body> and some content. This
|
||||||
|
// rules out 404s + verifies the MapRazorComponents route pipeline is wired.
|
||||||
|
var body = await page.Locator("body").InnerHTMLAsync();
|
||||||
|
body.Length.ShouldBeGreaterThan(0, "empty body = routing pipeline didn't hit Razor");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await fixture.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
<RootNamespace>ZB.MOM.WW.OtOpcUa.Admin.E2ETests</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="xunit.v3" Version="1.1.0"/>
|
||||||
|
<PackageReference Include="Shouldly" Version="4.3.0"/>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0"/>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.0"/>
|
||||||
|
<PackageReference Include="Microsoft.Playwright" Version="1.51.0"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\ZB.MOM.WW.OtOpcUa.Admin\ZB.MOM.WW.OtOpcUa.Admin.csproj"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-37gx-xxp4-5rgx"/>
|
||||||
|
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-w3x6-4m5h-cxqf"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user