From f60e098ca45c4c729c28924f054b1ef9c62bb30a Mon Sep 17 00:00:00 2001 From: Dragana Nikic Date: Tue, 3 Nov 2026 04:09:41 +0100 Subject: [PATCH 1/2] feat(LAB-02): add laboratory metadata model and tests --- .../LaboratoryTests.cs | 185 ++++++++++++++++++ .../Platform.LabMetadata.Tests.csproj | 27 +++ backend/Platform.LabMetadata/Laboratory.cs | 67 +++++++ .../LaboratoryDifficulty.cs | 10 + .../Platform.LabMetadata/LaboratoryType.cs | 15 ++ .../Platform.LabMetadata.csproj | 9 + backend/SecurityLab.slnx | 2 + 7 files changed, 315 insertions(+) create mode 100644 backend/Platform.LabMetadata.Tests/LaboratoryTests.cs create mode 100644 backend/Platform.LabMetadata.Tests/Platform.LabMetadata.Tests.csproj create mode 100644 backend/Platform.LabMetadata/Laboratory.cs create mode 100644 backend/Platform.LabMetadata/LaboratoryDifficulty.cs create mode 100644 backend/Platform.LabMetadata/LaboratoryType.cs create mode 100644 backend/Platform.LabMetadata/Platform.LabMetadata.csproj diff --git a/backend/Platform.LabMetadata.Tests/LaboratoryTests.cs b/backend/Platform.LabMetadata.Tests/LaboratoryTests.cs new file mode 100644 index 0000000..cb144df --- /dev/null +++ b/backend/Platform.LabMetadata.Tests/LaboratoryTests.cs @@ -0,0 +1,185 @@ + +using NUnit.Framework; +using Platform.LabMetadata; + +namespace Platform.LabMetadata.Tests +{ + [TestFixture] + public class LaboratoryTests + { + [Test] + public void Rename_ShouldNotChangeId() + { + // Arrange - priprema laboratorije + Guid id = Guid.NewGuid(); + + var laboratory = new Laboratory( + id, + "Original name", + "Learn access control", + LaboratoryDifficulty.Easy, + LaboratoryType.AccessControl, + new[] { "security" } + ); + + // Act - promena naziva + laboratory.Rename("Updated name"); + + // Assert - provera rezultata + Assert.That(laboratory.Id, Is.EqualTo(id)); + Assert.That(laboratory.Name, Is.EqualTo("Updated name")); + } + + + [Test] + public void Constructor_ShouldRejectInvalidDifficulty() + { + // Arrange + var invalidDifficulty = (LaboratoryDifficulty)999; + + // Act & Assert + Assert.Throws(() => + new Laboratory( + Guid.NewGuid(), + "Test laboratory", + "Test objective", + invalidDifficulty, + LaboratoryType.AccessControl, + new[] { "security" } + ) + ); + } + + + [Test] + public void Constructor_ShouldNormalizeTags() + { + // Arrange & Act + var laboratory = new Laboratory( + Guid.NewGuid(), + "Test laboratory", + "Test objective", + LaboratoryDifficulty.Easy, + LaboratoryType.AccessControl, + new[] { " Security ", "MFA", "security", " " } + ); + + // Assert + Assert.That( + laboratory.Tags, + Is.EqualTo(new[] { "security", "mfa" }) + ); + } + + + [Test] + public void Constructor_ShouldRejectInvalidType() + { + // Arrange + var invalidType = (LaboratoryType)999; + + // Act & Assert + Assert.Throws(() => + new Laboratory( + Guid.NewGuid(), + "Test laboratory", + "Test objective", + LaboratoryDifficulty.Easy, + invalidType, + new[] { "security" } + ) + ); + } + + + [Test] + public void Constructor_ShouldRejectEmptyId() + { + // Act & Assert + Assert.Throws(() => + new Laboratory( + Guid.Empty, + "Test laboratory", + "Test objective", + LaboratoryDifficulty.Easy, + LaboratoryType.AccessControl, + new[] { "security" } + ) + ); + } + + + [Test] + public void Constructor_ShouldRejectEmptyName() + { + // Act & Assert + Assert.Throws(() => + new Laboratory( + Guid.NewGuid(), + " ", + "Test objective", + LaboratoryDifficulty.Easy, + LaboratoryType.AccessControl, + new[] { "security" } + ) + ); + } + + + [Test] + public void Constructor_ShouldRejectEmptyObjective() + { + // Act & Assert + Assert.Throws(() => + new Laboratory( + Guid.NewGuid(), + "Test laboratory", + " ", + LaboratoryDifficulty.Easy, + LaboratoryType.AccessControl, + new[] { "security" } + ) + ); + } + + + [Test] + public void Rename_ShouldRejectEmptyName() + { + // Arrange + var laboratory = new Laboratory( + Guid.NewGuid(), + "Original name", + "Test objective", + LaboratoryDifficulty.Easy, + LaboratoryType.AccessControl, + new[] { "security" } + ); + + // Act & Assert + Assert.Throws(() => + laboratory.Rename(" ") + ); + + Assert.That(laboratory.Name, Is.EqualTo("Original name")); + } + + + [Test] + public void Constructor_ShouldRejectNullTags() + { + // Act & Assert + Assert.Throws(() => + new Laboratory( + Guid.NewGuid(), + "Test laboratory", + "Test objective", + LaboratoryDifficulty.Easy, + LaboratoryType.AccessControl, + null! + ) + ); + } + + } +} diff --git a/backend/Platform.LabMetadata.Tests/Platform.LabMetadata.Tests.csproj b/backend/Platform.LabMetadata.Tests/Platform.LabMetadata.Tests.csproj new file mode 100644 index 0000000..1d565f9 --- /dev/null +++ b/backend/Platform.LabMetadata.Tests/Platform.LabMetadata.Tests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/backend/Platform.LabMetadata/Laboratory.cs b/backend/Platform.LabMetadata/Laboratory.cs new file mode 100644 index 0000000..65775ad --- /dev/null +++ b/backend/Platform.LabMetadata/Laboratory.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Platform.LabMetadata +{ + public class Laboratory + { + public Guid Id { get; } + public string Name { get; private set; } + public string Objective { get; private set; } + public LaboratoryDifficulty Difficulty { get; private set; } + public LaboratoryType Type { get; private set; } + public IReadOnlyList Tags { get; private set; } = Array.Empty(); + + public Laboratory( + Guid id, + string name, + string objective, + LaboratoryDifficulty difficulty, + LaboratoryType type, + IEnumerable tags) + { + if (id == Guid.Empty) + throw new ArgumentException("Laboratory ID cannot be empty.", nameof(id)); + + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Laboratory name is required.", nameof(name)); + + if (string.IsNullOrWhiteSpace(objective)) + throw new ArgumentException("Laboratory objective is required.", nameof(objective)); + + if (!Enum.IsDefined(difficulty)) + throw new ArgumentOutOfRangeException(nameof(difficulty)); + + if (!Enum.IsDefined(type)) + throw new ArgumentOutOfRangeException(nameof(type)); + + Id = id; + Name = name.Trim(); + Objective = objective.Trim(); + Difficulty = difficulty; + Type = type; + + if (tags is null) + throw new ArgumentNullException(nameof(tags)); + + Tags = Array.AsReadOnly( + tags + .Where(tag => !string.IsNullOrWhiteSpace(tag)) + .Select(tag => tag.Trim().ToLowerInvariant()) + .Distinct() + .ToArray() + ); + } + + public void Rename(string newName) + { + if (string.IsNullOrWhiteSpace(newName)) + throw new ArgumentException( + "Laboratory name is required.", + nameof(newName)); + + Name = newName.Trim(); + } + } +} diff --git a/backend/Platform.LabMetadata/LaboratoryDifficulty.cs b/backend/Platform.LabMetadata/LaboratoryDifficulty.cs new file mode 100644 index 0000000..6f41dbc --- /dev/null +++ b/backend/Platform.LabMetadata/LaboratoryDifficulty.cs @@ -0,0 +1,10 @@ + +namespace Platform.LabMetadata +{ + public enum LaboratoryDifficulty + { + Easy, + Medium, + Hard + } +} diff --git a/backend/Platform.LabMetadata/LaboratoryType.cs b/backend/Platform.LabMetadata/LaboratoryType.cs new file mode 100644 index 0000000..01d31c0 --- /dev/null +++ b/backend/Platform.LabMetadata/LaboratoryType.cs @@ -0,0 +1,15 @@ + +namespace Platform.LabMetadata +{ + public enum LaboratoryType + { + AccessControl, + LogAnalysis, + SecurityConfiguration, + ThreatModeling, + MfaAndSessions, + ContextualAuthorization, + IncidentResponse, + IsolatedService + } +} diff --git a/backend/Platform.LabMetadata/Platform.LabMetadata.csproj b/backend/Platform.LabMetadata/Platform.LabMetadata.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/backend/Platform.LabMetadata/Platform.LabMetadata.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/backend/SecurityLab.slnx b/backend/SecurityLab.slnx index c8a3250..9cb062c 100644 --- a/backend/SecurityLab.slnx +++ b/backend/SecurityLab.slnx @@ -3,5 +3,7 @@ + + From 90a3fd14106e5d8d200d7edc503992f8b0f15723 Mon Sep 17 00:00:00 2001 From: Dragana Nikic Date: Wed, 4 Nov 2026 03:40:57 +0100 Subject: [PATCH 2/2] LAB-02: Align metadata model with LAB-01 identity --- .../LaboratoryTests.cs | 119 ++++-------------- .../{Laboratory.cs => LaboratoryMetadata.cs} | 50 ++++---- 2 files changed, 47 insertions(+), 122 deletions(-) rename backend/Platform.LabMetadata/{Laboratory.cs => LaboratoryMetadata.cs} (56%) diff --git a/backend/Platform.LabMetadata.Tests/LaboratoryTests.cs b/backend/Platform.LabMetadata.Tests/LaboratoryTests.cs index cb144df..b51bf44 100644 --- a/backend/Platform.LabMetadata.Tests/LaboratoryTests.cs +++ b/backend/Platform.LabMetadata.Tests/LaboratoryTests.cs @@ -1,47 +1,37 @@  +using System; using NUnit.Framework; using Platform.LabMetadata; namespace Platform.LabMetadata.Tests { [TestFixture] - public class LaboratoryTests + public class LaboratoryMetadataTests { [Test] - public void Rename_ShouldNotChangeId() + public void Constructor_ShouldPreserveLaboratoryId() { - // Arrange - priprema laboratorije - Guid id = Guid.NewGuid(); + var id = Guid.NewGuid(); - var laboratory = new Laboratory( + var metadata = new LaboratoryMetadata( id, - "Original name", "Learn access control", LaboratoryDifficulty.Easy, LaboratoryType.AccessControl, new[] { "security" } ); - // Act - promena naziva - laboratory.Rename("Updated name"); - - // Assert - provera rezultata - Assert.That(laboratory.Id, Is.EqualTo(id)); - Assert.That(laboratory.Name, Is.EqualTo("Updated name")); + Assert.That(metadata.LaboratoryId, Is.EqualTo(id)); } - [Test] public void Constructor_ShouldRejectInvalidDifficulty() { - // Arrange var invalidDifficulty = (LaboratoryDifficulty)999; - // Act & Assert Assert.Throws(() => - new Laboratory( + new LaboratoryMetadata( Guid.NewGuid(), - "Test laboratory", "Test objective", invalidDifficulty, LaboratoryType.AccessControl, @@ -50,39 +40,14 @@ public void Constructor_ShouldRejectInvalidDifficulty() ); } - - [Test] - public void Constructor_ShouldNormalizeTags() - { - // Arrange & Act - var laboratory = new Laboratory( - Guid.NewGuid(), - "Test laboratory", - "Test objective", - LaboratoryDifficulty.Easy, - LaboratoryType.AccessControl, - new[] { " Security ", "MFA", "security", " " } - ); - - // Assert - Assert.That( - laboratory.Tags, - Is.EqualTo(new[] { "security", "mfa" }) - ); - } - - [Test] public void Constructor_ShouldRejectInvalidType() { - // Arrange var invalidType = (LaboratoryType)999; - // Act & Assert Assert.Throws(() => - new Laboratory( + new LaboratoryMetadata( Guid.NewGuid(), - "Test laboratory", "Test objective", LaboratoryDifficulty.Easy, invalidType, @@ -91,32 +56,29 @@ public void Constructor_ShouldRejectInvalidType() ); } - [Test] - public void Constructor_ShouldRejectEmptyId() + public void Constructor_ShouldNormalizeTags() { - // Act & Assert - Assert.Throws(() => - new Laboratory( - Guid.Empty, - "Test laboratory", - "Test objective", - LaboratoryDifficulty.Easy, - LaboratoryType.AccessControl, - new[] { "security" } - ) + var metadata = new LaboratoryMetadata( + Guid.NewGuid(), + "Test objective", + LaboratoryDifficulty.Easy, + LaboratoryType.AccessControl, + new[] { " Security ", "MFA", "security", " " } ); - } + Assert.That( + metadata.Tags, + Is.EqualTo(new[] { "security", "mfa" }) + ); + } [Test] - public void Constructor_ShouldRejectEmptyName() + public void Constructor_ShouldRejectEmptyId() { - // Act & Assert Assert.Throws(() => - new Laboratory( - Guid.NewGuid(), - " ", + new LaboratoryMetadata( + Guid.Empty, "Test objective", LaboratoryDifficulty.Easy, LaboratoryType.AccessControl, @@ -125,16 +87,13 @@ public void Constructor_ShouldRejectEmptyName() ); } - [Test] public void Constructor_ShouldRejectEmptyObjective() { - // Act & Assert Assert.Throws(() => - new Laboratory( + new LaboratoryMetadata( Guid.NewGuid(), - "Test laboratory", - " ", + " ", LaboratoryDifficulty.Easy, LaboratoryType.AccessControl, new[] { "security" } @@ -142,37 +101,12 @@ public void Constructor_ShouldRejectEmptyObjective() ); } - - [Test] - public void Rename_ShouldRejectEmptyName() - { - // Arrange - var laboratory = new Laboratory( - Guid.NewGuid(), - "Original name", - "Test objective", - LaboratoryDifficulty.Easy, - LaboratoryType.AccessControl, - new[] { "security" } - ); - - // Act & Assert - Assert.Throws(() => - laboratory.Rename(" ") - ); - - Assert.That(laboratory.Name, Is.EqualTo("Original name")); - } - - [Test] public void Constructor_ShouldRejectNullTags() { - // Act & Assert Assert.Throws(() => - new Laboratory( + new LaboratoryMetadata( Guid.NewGuid(), - "Test laboratory", "Test objective", LaboratoryDifficulty.Easy, LaboratoryType.AccessControl, @@ -180,6 +114,5 @@ public void Constructor_ShouldRejectNullTags() ) ); } - } } diff --git a/backend/Platform.LabMetadata/Laboratory.cs b/backend/Platform.LabMetadata/LaboratoryMetadata.cs similarity index 56% rename from backend/Platform.LabMetadata/Laboratory.cs rename to backend/Platform.LabMetadata/LaboratoryMetadata.cs index 65775ad..25f6894 100644 --- a/backend/Platform.LabMetadata/Laboratory.cs +++ b/backend/Platform.LabMetadata/LaboratoryMetadata.cs @@ -1,34 +1,37 @@ -using System; + +using System; using System.Collections.Generic; using System.Linq; namespace Platform.LabMetadata { - public class Laboratory + public class LaboratoryMetadata { - public Guid Id { get; } - public string Name { get; private set; } + // Referenca na postojecu laboratoriju iz LAB-01. + // LAB-02 ne generise novi identitet. + public Guid LaboratoryId { get; } + public string Objective { get; private set; } public LaboratoryDifficulty Difficulty { get; private set; } public LaboratoryType Type { get; private set; } - public IReadOnlyList Tags { get; private set; } = Array.Empty(); + public IReadOnlyList Tags { get; private set; } - public Laboratory( - Guid id, - string name, + public LaboratoryMetadata( + Guid laboratoryId, string objective, LaboratoryDifficulty difficulty, LaboratoryType type, IEnumerable tags) { - if (id == Guid.Empty) - throw new ArgumentException("Laboratory ID cannot be empty.", nameof(id)); - - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentException("Laboratory name is required.", nameof(name)); + if (laboratoryId == Guid.Empty) + throw new ArgumentException( + "Laboratory ID cannot be empty.", + nameof(laboratoryId)); if (string.IsNullOrWhiteSpace(objective)) - throw new ArgumentException("Laboratory objective is required.", nameof(objective)); + throw new ArgumentException( + "Laboratory objective is required.", + nameof(objective)); if (!Enum.IsDefined(difficulty)) throw new ArgumentOutOfRangeException(nameof(difficulty)); @@ -36,15 +39,14 @@ public Laboratory( if (!Enum.IsDefined(type)) throw new ArgumentOutOfRangeException(nameof(type)); - Id = id; - Name = name.Trim(); + if (tags is null) + throw new ArgumentNullException(nameof(tags)); + + LaboratoryId = laboratoryId; Objective = objective.Trim(); Difficulty = difficulty; Type = type; - if (tags is null) - throw new ArgumentNullException(nameof(tags)); - Tags = Array.AsReadOnly( tags .Where(tag => !string.IsNullOrWhiteSpace(tag)) @@ -53,15 +55,5 @@ public Laboratory( .ToArray() ); } - - public void Rename(string newName) - { - if (string.IsNullOrWhiteSpace(newName)) - throw new ArgumentException( - "Laboratory name is required.", - nameof(newName)); - - Name = newName.Trim(); - } } }