Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions roles/common/files/fwo-api-calls/request/getActions.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,12 @@ query getActions {
event
button_text
external_parameters
state_actions(order_by: { sort_order: asc, state_id: asc }) {
sort_order
state {
id
name
}
}
}
}
6 changes: 6 additions & 0 deletions roles/database/files/sql/idempotent/fworch-texts.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2840,6 +2840,8 @@ INSERT INTO txt VALUES ('add_derived_state', 'German', 'Abgeleiteten Status
INSERT INTO txt VALUES ('add_derived_state', 'English', 'Add derived state');
INSERT INTO txt VALUES ('edit_derived_state', 'German', 'Abgeleiteten Status bearbeiten');
INSERT INTO txt VALUES ('edit_derived_state', 'English', 'Edit derived state');
INSERT INTO txt VALUES ('using_states', 'German', 'Verwendende Status');
INSERT INTO txt VALUES ('using_states', 'English', 'Using states');
INSERT INTO txt VALUES ('ext_states', 'German', 'Externe Status');
INSERT INTO txt VALUES ('ext_states', 'English', 'External states');
INSERT INTO txt VALUES ('static_external_states','German', 'Statische externe Status');
Expand Down Expand Up @@ -3645,6 +3647,8 @@ INSERT INTO txt VALUES ('import_subnet_data', 'German', 'Subnetzdaten-Import'
INSERT INTO txt VALUES ('import_subnet_data', 'English', 'Subnet Data Import');
INSERT INTO txt VALUES ('general', 'German', 'Allgemein');
INSERT INTO txt VALUES ('general', 'English', 'General');
INSERT INTO txt VALUES ('action_specific', 'German', 'Aktion spezifisch');
INSERT INTO txt VALUES ('action_specific', 'English', 'Action specific');
INSERT INTO txt VALUES ('naming_convention', 'German', 'Namenskonvention');
INSERT INTO txt VALUES ('naming_convention', 'English', 'Naming Convention');
INSERT INTO txt VALUES ('import_app_server', 'German', 'App Server importieren');
Expand Down Expand Up @@ -6454,6 +6458,8 @@ INSERT INTO txt VALUES ('H5536', 'German', 'Flow-Erzeugung per UI-Meldung best&
INSERT INTO txt VALUES ('H5536', 'English', 'Confirm flow creation via UI message: After flow creation, the action shows a UI message. For failures, the message points to the workflow log with details about unresolved objects or services.');
INSERT INTO txt VALUES ('H5537', 'German', 'Saubere Zonen: Wenn aktiviert, werden Aufgaben nur gebündelt, wenn ihre Quell- und Zielobjekte anhand der ausgewählten Policy-Matrix jeweils denselben Netzwerkzonen zugeordnet werden können. Ohne ausgewählte Policy oder ohne Matrix in der Policy wird keine saubere Zonenübereinstimmung angenommen.');
INSERT INTO txt VALUES ('H5537', 'English', 'Clean zones: When enabled, tasks are bundled only if their source and destination objects can be mapped to the same network zones using the selected policy matrix. Without a selected policy or without a matrix in the policy, no clean zone match is assumed.');
INSERT INTO txt VALUES ('H5538', 'German', 'Die Aktionsübersicht zeigt zusätzlich an, in wie vielen Status die jeweilige Aktion verwendet wird. Der Bearbeitungsdialog ist in die Bereiche "Allgemein", "Aktion spezifisch" und "Verwendende Status" gegliedert; dort werden die zugeordneten Status aufgelistet und können direkt hinzugefügt oder entfernt werden.');
INSERT INTO txt VALUES ('H5538', 'English', 'The action overview additionally shows how many states use each action. The edit dialog is split into "General", "Action specific", and "Using states"; the linked states are listed there and can be added or removed directly.');
INSERT INTO txt VALUES ('H5541', 'German', 'In der Status-Matrix werden die verarbeitbaren Status pro Phase und Tasktyp festgelegt.
Es gibt eine Master-Matrix, welche die Eigenschaften auf Ticket-Ebene beschreibt, sowie und für jeden Tasktyp separate Matrizen.
Oberhalb der Konfigurationsauswahl können Sichtbarkeitsgruppen und Übergangsgruppen bearbeitet sowie neue leere Konfigurationen oder Kopien einer ausgewählten Konfiguration angelegt werden. Nur inaktive Konfigurationen können gelöscht werden; nicht mehr verwendete Phasenmatrizen werden dabei ebenfalls entfernt. Mitglieder von Sichtbarkeitsgruppen können als DN eingegeben oder über die Benutzer- und Gruppensuche ausgewählt werden. Beim Löschen einer Sichtbarkeitsgruppe wird ihre Zuordnung zu Übergangsgruppen entfernt. Beim Löschen einer Übergangsgruppe werden auch ihre Übergänge und Phasenzuordnungen gelöscht. Genau eine Konfiguration ist aktiv und wird zur Laufzeit verwendet.
Expand Down
36 changes: 36 additions & 0 deletions roles/lib/files/FWO.Data/Workflow/WfStateAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ public class WfStateAction
[JsonProperty("external_parameters"), JsonPropertyName("external_parameters")]
public string ExternalParams { get; set; } = "";

private List<WfStateActionStateHelper> _stateActions = [];

[JsonProperty("state_actions"), JsonPropertyName("state_actions")]
public List<WfStateActionStateHelper> StateActions
{
get => _stateActions;
set => _stateActions = value ?? [];
}

public int StateCount => StateActions.Count;


public WfStateAction()
{ }
Expand All @@ -90,6 +101,7 @@ public WfStateAction(WfStateAction action)
Event = action.Event;
ButtonText = action.ButtonText;
ExternalParams = action.ExternalParams;
StateActions = [.. action.StateActions.Select(stateAction => new WfStateActionStateHelper(stateAction))];
}

public static bool IsReadonlyType(string actionTypeString)
Expand Down Expand Up @@ -214,4 +226,28 @@ public class WfStateActionDataHelper
[JsonProperty("action"), JsonPropertyName("action")]
public WfStateAction Action { get; set; } = new WfStateAction();
}

public class WfStateActionStateHelper
{
private WfState _state = new();

[JsonProperty("sort_order"), JsonPropertyName("sort_order")]
public int SortOrder { get; set; }

[JsonProperty("state"), JsonPropertyName("state")]
public WfState State
{
get => _state;
set => _state = value ?? new WfState();
}

public WfStateActionStateHelper()
{ }

public WfStateActionStateHelper(WfStateActionStateHelper stateActionStateHelper)
{
SortOrder = stateActionStateHelper.SortOrder;
State = new WfState(stateActionStateHelper.State);
}
}
}
41 changes: 41 additions & 0 deletions roles/tests-unit/files/FWO.Test/DisplayServiceTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FWO.Config.Api;
using FWO.Data.Workflow;
using FWO.Ui.Services;
using Microsoft.AspNetCore.Components;
using NUnit.Framework;
Expand Down Expand Up @@ -68,5 +69,45 @@ public void DisplayButtonWithTooltip_IncludesTextInTooltip_WhenIconifyEnabled()
Assert.That(result.Value, Does.Contain("<span class=\"stdtext\">Add</span>"));
Assert.That(result.Value, Does.Contain("<span class=\"bi bi-obj\"/>"));
}

[Test]
public void DisplayState_ReturnsAutomaticForAutomaticState()
{
UserConfig userConfig = new SimulatedUserConfig();

string result = DisplayService.DisplayState(userConfig, new WfState { Id = -1, Name = "Ignored" });

Assert.That(result, Is.EqualTo(userConfig.GetText("automatic")));
}

[Test]
public void DisplayState_ReturnsConditionalForConditionalState()
{
UserConfig userConfig = new SimulatedUserConfig();

string result = DisplayService.DisplayState(userConfig, new WfState { Id = -2, Name = "Ignored" });

Assert.That(result, Is.EqualTo(userConfig.GetText("Conditional")));
}

[Test]
public void DisplayStateWithId_ReturnsNameAndIdForPositiveState()
{
UserConfig userConfig = new SimulatedUserConfig();

string result = DisplayService.DisplayStateWithId(userConfig, new WfState { Id = 17, Name = "Approved" });

Assert.That(result, Is.EqualTo("Approved (17)"));
}

[Test]
public void DisplayStateWithId_DoesNotAppendIdForNegativeState()
{
UserConfig userConfig = new SimulatedUserConfig();

string result = DisplayService.DisplayStateWithId(userConfig, new WfState { Id = -1, Name = "Ignored" });

Assert.That(result, Is.EqualTo(userConfig.GetText("automatic")));
}
}
}
130 changes: 130 additions & 0 deletions roles/tests-unit/files/FWO.Test/ExecutionModeStorageTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using FWO.Basics;
using FWO.Test.Mocks;
using FWO.Ui.Services;
using Microsoft.AspNetCore.Session;
using NUnit.Framework;
using System.Reflection;

namespace FWO.Test
{
[TestFixture]
internal class ExecutionModeStorageTest
{
[Test]
public async Task GetExecutionModeReturnsStoredValue()
{
MockProtectedSessionStorage sessionStorage = new();
ExecutionModeStorage storage = new(sessionStorage);

await storage.SetExecutionMode(Roles.Admin);

string? result = await storage.GetExecutionMode();

Assert.That(result, Is.EqualTo(Roles.Admin));
}

[Test]
public async Task GetExecutionModeReturnsNullForWhitespaceAndMissingEntries()
{
MockProtectedSessionStorage sessionStorage = new();
ExecutionModeStorage storage = new(sessionStorage);

await sessionStorage.SetAsync("execution_mode", " ");
string? whitespaceResult = await storage.GetExecutionMode();
await sessionStorage.DeleteAsync("execution_mode");
string? missingResult = await storage.GetExecutionMode();

Assert.Multiple(() =>
{
Assert.That(whitespaceResult, Is.Null);
Assert.That(missingResult, Is.Null);
});
}

[Test]
public async Task SetExecutionModeStoresUserRoleSelectionWhenInputIsEmpty()
{
MockProtectedSessionStorage sessionStorage = new();
ExecutionModeStorage storage = new(sessionStorage);

await storage.SetExecutionMode("");

string? result = await storage.GetExecutionMode();

Assert.That(result, Is.EqualTo(GlobalConst.kUserRolesSelection));
}

[Test]
public async Task GetExecutionModeClearsStorageAndReturnsNullWhenSessionStorageThrows()
{
ThrowingSessionStorage sessionStorage = new(getException: new InvalidOperationException("get failed"));
ExecutionModeStorage storage = new(sessionStorage);

string? result = await storage.GetExecutionMode();

Assert.Multiple(() =>
{
Assert.That(result, Is.Null);
Assert.That(sessionStorage.DeleteCallCount, Is.EqualTo(1));
});
}

[Test]
public async Task ClearExecutionModeSwallowsDeleteFailures()
{
ThrowingSessionStorage sessionStorage = new(deleteException: new InvalidOperationException("delete failed"));
ExecutionModeStorage storage = new(sessionStorage);

Assert.DoesNotThrowAsync(async () => await storage.ClearExecutionMode());
Assert.That(sessionStorage.DeleteCallCount, Is.EqualTo(1));
}

private sealed class ThrowingSessionStorage : ISessionStorage
{
private readonly Exception? getException;
private readonly Exception? deleteException;

public int DeleteCallCount { get; private set; }

public ThrowingSessionStorage(Exception? getException = null, Exception? deleteException = null)
{
this.getException = getException;
this.deleteException = deleteException;
}

public ValueTask<Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.ProtectedBrowserStorageResult<TValue>> GetAsync<TValue>(string key)
{
if (getException != null)
{
throw getException;
}

return ValueTask.FromResult(CreateFailureResult<TValue>());
}

public ValueTask SetAsync(string key, object value)
{
return ValueTask.CompletedTask;
}

public ValueTask DeleteAsync(string key)
{
DeleteCallCount++;
if (deleteException != null)
{
throw deleteException;
}

return ValueTask.CompletedTask;
}

private static Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.ProtectedBrowserStorageResult<TValue> CreateFailureResult<TValue>()
{
var constructor = typeof(Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.ProtectedBrowserStorageResult<TValue>).GetConstructors(
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)[0];

return (Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.ProtectedBrowserStorageResult<TValue>)constructor.Invoke([false, default(TValue)]);
}
}
}
}
Loading
Loading