From b18a244a54b49700da0ee46a837aef89047b81b6 Mon Sep 17 00:00:00 2001 From: danijela76 Date: Mon, 2 Nov 2026 23:26:16 +0100 Subject: [PATCH 1/5] LAB-06: dodati projekti za preduslove i testove --- .../Platform.Prerequisites.Tests.csproj | 27 +++++++++++++++++++ .../Platform.Prerequisites.csproj | 9 +++++++ backend/SecurityLab.slnx | 2 ++ 3 files changed, 38 insertions(+) create mode 100644 backend/Platform.Prerequisites.Tests/Platform.Prerequisites.Tests.csproj create mode 100644 backend/Platform.Prerequisites/Platform.Prerequisites.csproj diff --git a/backend/Platform.Prerequisites.Tests/Platform.Prerequisites.Tests.csproj b/backend/Platform.Prerequisites.Tests/Platform.Prerequisites.Tests.csproj new file mode 100644 index 0000000..b0d927d --- /dev/null +++ b/backend/Platform.Prerequisites.Tests/Platform.Prerequisites.Tests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/backend/Platform.Prerequisites/Platform.Prerequisites.csproj b/backend/Platform.Prerequisites/Platform.Prerequisites.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/backend/Platform.Prerequisites/Platform.Prerequisites.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/backend/SecurityLab.slnx b/backend/SecurityLab.slnx index c8a3250..7194f0d 100644 --- a/backend/SecurityLab.slnx +++ b/backend/SecurityLab.slnx @@ -3,5 +3,7 @@ + + From 85b2095be0b948e7a04003d50787622e28dfbd5a Mon Sep 17 00:00:00 2001 From: danijela76 Date: Mon, 2 Nov 2026 23:32:40 +0100 Subject: [PATCH 2/5] LAB-06: model preduslova i zabrana sopstvenog preduslova --- .../LabPrerequisiteTests.cs | 39 +++++++++++++++++++ .../Domain/LabPrerequisite.cs | 23 +++++++++++ 2 files changed, 62 insertions(+) create mode 100644 backend/Platform.Prerequisites.Tests/LabPrerequisiteTests.cs create mode 100644 backend/Platform.Prerequisites/Domain/LabPrerequisite.cs diff --git a/backend/Platform.Prerequisites.Tests/LabPrerequisiteTests.cs b/backend/Platform.Prerequisites.Tests/LabPrerequisiteTests.cs new file mode 100644 index 0000000..e455c82 --- /dev/null +++ b/backend/Platform.Prerequisites.Tests/LabPrerequisiteTests.cs @@ -0,0 +1,39 @@ +using NUnit.Framework; +using Platform.Prerequisites.Domain; + +namespace Platform.Prerequisites.Tests; + +[TestFixture] +public class LabPrerequisiteTests +{ + [Test] + public void Constructor_ValidLaboratories_SavesBothIds() + { + Guid laboratoryId = Guid.NewGuid(); + Guid requiredId = Guid.NewGuid(); + + var prerequisite = new LabPrerequisite(laboratoryId, requiredId); + + Assert.Multiple(() => + { + Assert.That(prerequisite.LaboratoryId, Is.EqualTo(laboratoryId)); + Assert.That(prerequisite.RequiredLaboratoryId, Is.EqualTo(requiredId)); + }); + } + + [Test] + public void Constructor_SameLaboratory_IsRejected() + { + Guid laboratoryId = Guid.NewGuid(); + + Assert.Throws( + () => new LabPrerequisite(laboratoryId, laboratoryId)); + } + + [Test] + public void Constructor_EmptyId_IsRejected() + { + Assert.Throws( + () => new LabPrerequisite(Guid.Empty, Guid.NewGuid())); + } +} \ No newline at end of file diff --git a/backend/Platform.Prerequisites/Domain/LabPrerequisite.cs b/backend/Platform.Prerequisites/Domain/LabPrerequisite.cs new file mode 100644 index 0000000..265f9e6 --- /dev/null +++ b/backend/Platform.Prerequisites/Domain/LabPrerequisite.cs @@ -0,0 +1,23 @@ +namespace Platform.Prerequisites.Domain; + +public sealed class LabPrerequisite +{ + public Guid LaboratoryId { get; } + public Guid RequiredLaboratoryId { get; } + + public LabPrerequisite(Guid laboratoryId, Guid requiredLaboratoryId) + { + if (laboratoryId == Guid.Empty || requiredLaboratoryId == Guid.Empty) + { + throw new ArgumentException("ID laboratorije ne sme biti prazan."); + } + + if (laboratoryId == requiredLaboratoryId) + { + throw new ArgumentException("Laboratorija ne može biti sopstveni preduslov."); + } + + LaboratoryId = laboratoryId; + RequiredLaboratoryId = requiredLaboratoryId; + } +} \ No newline at end of file From 9f06b45606564e1d6a107e9ca4ee391eb6241fb1 Mon Sep 17 00:00:00 2001 From: danijela76 Date: Mon, 2 Nov 2026 23:37:10 +0100 Subject: [PATCH 3/5] LAB-06: zabrana duplikata i ciklusa preduslova --- .../PrerequisiteRulesTests.cs | 106 ++++++++++++++++++ .../Domain/PrerequisiteRules.cs | 47 ++++++++ 2 files changed, 153 insertions(+) create mode 100644 backend/Platform.Prerequisites.Tests/PrerequisiteRulesTests.cs create mode 100644 backend/Platform.Prerequisites/Domain/PrerequisiteRules.cs diff --git a/backend/Platform.Prerequisites.Tests/PrerequisiteRulesTests.cs b/backend/Platform.Prerequisites.Tests/PrerequisiteRulesTests.cs new file mode 100644 index 0000000..950271d --- /dev/null +++ b/backend/Platform.Prerequisites.Tests/PrerequisiteRulesTests.cs @@ -0,0 +1,106 @@ +using NUnit.Framework; +using Platform.Prerequisites.Domain; + +namespace Platform.Prerequisites.Tests; + +[TestFixture] +public class PrerequisiteRulesTests +{ + [Test] + public void IsDuplicate_SamePairExists_ReturnsTrue() + { + Guid labA = Guid.NewGuid(); + Guid labB = Guid.NewGuid(); + var existing = new[] { new LabPrerequisite(labA, labB) }; + + bool result = PrerequisiteRules.IsDuplicate( + existing, + new LabPrerequisite(labA, labB)); + + Assert.That(result, Is.True); + } + + [Test] + public void IsDuplicate_DifferentPair_ReturnsFalse() + { + Guid labA = Guid.NewGuid(); + Guid labB = Guid.NewGuid(); + Guid labC = Guid.NewGuid(); + var existing = new[] { new LabPrerequisite(labA, labB) }; + + bool result = PrerequisiteRules.IsDuplicate( + existing, + new LabPrerequisite(labA, labC)); + + Assert.That(result, Is.False); + } + + [Test] + public void WouldCreateCycle_TwoLaboratories_ReturnsTrue() + { + Guid labA = Guid.NewGuid(); + Guid labB = Guid.NewGuid(); + var existing = new[] { new LabPrerequisite(labA, labB) }; + + bool result = PrerequisiteRules.WouldCreateCycle( + existing, + new LabPrerequisite(labB, labA)); + + Assert.That(result, Is.True); + } + + [Test] + public void WouldCreateCycle_ThreeLaboratories_ReturnsTrue() + { + Guid labA = Guid.NewGuid(); + Guid labB = Guid.NewGuid(); + Guid labC = Guid.NewGuid(); + var existing = new[] + { + new LabPrerequisite(labA, labB), + new LabPrerequisite(labB, labC) + }; + + bool result = PrerequisiteRules.WouldCreateCycle( + existing, + new LabPrerequisite(labC, labA)); + + Assert.That(result, Is.True); + } + + [Test] + public void WouldCreateCycle_NormalChain_ReturnsFalse() + { + Guid labA = Guid.NewGuid(); + Guid labB = Guid.NewGuid(); + Guid labC = Guid.NewGuid(); + var existing = new[] { new LabPrerequisite(labA, labB) }; + + bool result = PrerequisiteRules.WouldCreateCycle( + existing, + new LabPrerequisite(labB, labC)); + + Assert.That(result, Is.False); + } + + [Test] + public void WouldCreateCycle_SharedPrerequisite_ReturnsFalse() + { + Guid labA = Guid.NewGuid(); + Guid labB = Guid.NewGuid(); + Guid labC = Guid.NewGuid(); + Guid labD = Guid.NewGuid(); + var existing = new[] + { + new LabPrerequisite(labA, labB), + new LabPrerequisite(labA, labC), + new LabPrerequisite(labB, labD) + }; + + bool result = PrerequisiteRules.WouldCreateCycle( + existing, + new LabPrerequisite(labC, labD)); + + Assert.That(result, Is.False); + } +} diff --git a/backend/Platform.Prerequisites/Domain/PrerequisiteRules.cs b/backend/Platform.Prerequisites/Domain/PrerequisiteRules.cs new file mode 100644 index 0000000..806228c --- /dev/null +++ b/backend/Platform.Prerequisites/Domain/PrerequisiteRules.cs @@ -0,0 +1,47 @@ +namespace Platform.Prerequisites.Domain; + +public static class PrerequisiteRules +{ + public static bool IsDuplicate( + IEnumerable existing, + LabPrerequisite candidate) + { + return existing.Any(prerequisite => + prerequisite.LaboratoryId == candidate.LaboratoryId && + prerequisite.RequiredLaboratoryId == candidate.RequiredLaboratoryId); + } + + public static bool WouldCreateCycle( + IReadOnlyCollection existing, + LabPrerequisite candidate) + { + var visited = new HashSet(); + var toVisit = new Stack(); + toVisit.Push(candidate.RequiredLaboratoryId); + + while (toVisit.Count > 0) + { + Guid current = toVisit.Pop(); + + if (current == candidate.LaboratoryId) + { + return true; + } + + if (!visited.Add(current)) + { + continue; + } + + foreach (LabPrerequisite prerequisite in existing) + { + if (prerequisite.LaboratoryId == current) + { + toVisit.Push(prerequisite.RequiredLaboratoryId); + } + } + } + + return false; + } +} \ No newline at end of file From a1ab20f28aca555eb1f4e277f99b36928139aad5 Mon Sep 17 00:00:00 2001 From: danijela76 Date: Mon, 2 Nov 2026 23:40:38 +0100 Subject: [PATCH 4/5] LAB-06: interfejs i stub za zavrsene laboratorije --- .../StubCompletedLaboratoriesTests.cs | 32 +++++++++++++++++++ .../Contracts/ICompletedLaboratories.cs | 8 +++++ .../StubCompletedLaboratories.cs | 30 +++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 backend/Platform.Prerequisites.Tests/StubCompletedLaboratoriesTests.cs create mode 100644 backend/Platform.Prerequisites/Contracts/ICompletedLaboratories.cs create mode 100644 backend/Platform.Prerequisites/Infrastructure/StubCompletedLaboratories.cs diff --git a/backend/Platform.Prerequisites.Tests/StubCompletedLaboratoriesTests.cs b/backend/Platform.Prerequisites.Tests/StubCompletedLaboratoriesTests.cs new file mode 100644 index 0000000..2aa9697 --- /dev/null +++ b/backend/Platform.Prerequisites.Tests/StubCompletedLaboratoriesTests.cs @@ -0,0 +1,32 @@ +using NUnit.Framework; +using Platform.Prerequisites.Infrastructure; + +namespace Platform.Prerequisites.Tests; + +[TestFixture] +public class StubCompletedLaboratoriesTests +{ + [Test] + public async Task GetCompletedAsync_ReturnsMarkedLaboratory() + { + var stub = new StubCompletedLaboratories(); + Guid studentId = Guid.NewGuid(); + Guid laboratoryId = Guid.NewGuid(); + + stub.MarkCompleted(studentId, laboratoryId); + IReadOnlySet completed = await stub.GetCompletedAsync(studentId); + + Assert.That(completed, Does.Contain(laboratoryId)); + } + + [Test] + public async Task GetCompletedAsync_OtherStudent_ReturnsEmpty() + { + var stub = new StubCompletedLaboratories(); + stub.MarkCompleted(Guid.NewGuid(), Guid.NewGuid()); + + IReadOnlySet completed = await stub.GetCompletedAsync(Guid.NewGuid()); + + Assert.That(completed, Is.Empty); + } +} \ No newline at end of file diff --git a/backend/Platform.Prerequisites/Contracts/ICompletedLaboratories.cs b/backend/Platform.Prerequisites/Contracts/ICompletedLaboratories.cs new file mode 100644 index 0000000..6782f01 --- /dev/null +++ b/backend/Platform.Prerequisites/Contracts/ICompletedLaboratories.cs @@ -0,0 +1,8 @@ +namespace Platform.Prerequisites.Contracts; + +public interface ICompletedLaboratories +{ + Task> GetCompletedAsync( + Guid studentId, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/backend/Platform.Prerequisites/Infrastructure/StubCompletedLaboratories.cs b/backend/Platform.Prerequisites/Infrastructure/StubCompletedLaboratories.cs new file mode 100644 index 0000000..b7875bb --- /dev/null +++ b/backend/Platform.Prerequisites/Infrastructure/StubCompletedLaboratories.cs @@ -0,0 +1,30 @@ +using Platform.Prerequisites.Contracts; + +namespace Platform.Prerequisites.Infrastructure; + +public sealed class StubCompletedLaboratories : ICompletedLaboratories +{ + private readonly Dictionary> _completed = new(); + + public void MarkCompleted(Guid studentId, Guid laboratoryId) + { + if (!_completed.TryGetValue(studentId, out HashSet? laboratories)) + { + laboratories = new HashSet(); + _completed[studentId] = laboratories; + } + + laboratories.Add(laboratoryId); + } + + public Task> GetCompletedAsync( + Guid studentId, + CancellationToken cancellationToken = default) + { + IReadOnlySet result = _completed.TryGetValue(studentId, out HashSet? laboratories) + ? new HashSet(laboratories) + : new HashSet(); + + return Task.FromResult(result); + } +} \ No newline at end of file From db284408083d5325ea2c744a82b5a3c541897705 Mon Sep 17 00:00:00 2001 From: danijela76 Date: Mon, 2 Nov 2026 23:43:33 +0100 Subject: [PATCH 5/5] LAB-06: repozitorijum preduslova u memoriji --- .../InMemoryPrerequisiteRepositoryTests.cs | 88 +++++++++++++++++++ .../Contracts/IPrerequisiteRepository.cs | 18 ++++ .../Domain/AddPrerequisiteResult.cs | 8 ++ .../InMemoryPrerequisiteRepository.cs | 59 +++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 backend/Platform.Prerequisites.Tests/InMemoryPrerequisiteRepositoryTests.cs create mode 100644 backend/Platform.Prerequisites/Contracts/IPrerequisiteRepository.cs create mode 100644 backend/Platform.Prerequisites/Domain/AddPrerequisiteResult.cs create mode 100644 backend/Platform.Prerequisites/Infrastructure/InMemoryPrerequisiteRepository.cs diff --git a/backend/Platform.Prerequisites.Tests/InMemoryPrerequisiteRepositoryTests.cs b/backend/Platform.Prerequisites.Tests/InMemoryPrerequisiteRepositoryTests.cs new file mode 100644 index 0000000..3a09b92 --- /dev/null +++ b/backend/Platform.Prerequisites.Tests/InMemoryPrerequisiteRepositoryTests.cs @@ -0,0 +1,88 @@ +using NUnit.Framework; +using Platform.Prerequisites.Domain; +using Platform.Prerequisites.Infrastructure; + +namespace Platform.Prerequisites.Tests; + +[TestFixture] +public class InMemoryPrerequisiteRepositoryTests +{ + private readonly Guid _labA = Guid.NewGuid(); + private readonly Guid _labB = Guid.NewGuid(); + + [Test] + public async Task AddAsync_NewPrerequisite_IsSaved() + { + var repository = new InMemoryPrerequisiteRepository(); + + AddPrerequisiteResult result = + await repository.AddAsync(new LabPrerequisite(_labA, _labB)); + var saved = await repository.GetForLaboratoryAsync(_labA); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(AddPrerequisiteResult.Added)); + Assert.That(saved, Has.Count.EqualTo(1)); + }); + } + + [Test] + public async Task AddAsync_Duplicate_IsRejected() + { + var repository = new InMemoryPrerequisiteRepository(); + await repository.AddAsync(new LabPrerequisite(_labA, _labB)); + + AddPrerequisiteResult result = + await repository.AddAsync(new LabPrerequisite(_labA, _labB)); + var saved = await repository.GetForLaboratoryAsync(_labA); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(AddPrerequisiteResult.Duplicate)); + Assert.That(saved, Has.Count.EqualTo(1)); + }); + } + + [Test] + public async Task AddAsync_Cycle_IsRejected() + { + var repository = new InMemoryPrerequisiteRepository(); + await repository.AddAsync(new LabPrerequisite(_labA, _labB)); + + AddPrerequisiteResult result = + await repository.AddAsync(new LabPrerequisite(_labB, _labA)); + var saved = await repository.GetForLaboratoryAsync(_labB); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(AddPrerequisiteResult.WouldCreateCycle)); + Assert.That(saved, Is.Empty); + }); + } + + [Test] + public async Task RemoveAsync_ExistingPrerequisite_ReturnsTrue() + { + var repository = new InMemoryPrerequisiteRepository(); + await repository.AddAsync(new LabPrerequisite(_labA, _labB)); + + bool removed = await repository.RemoveAsync(new LabPrerequisite(_labA, _labB)); + var saved = await repository.GetForLaboratoryAsync(_labA); + + Assert.Multiple(() => + { + Assert.That(removed, Is.True); + Assert.That(saved, Is.Empty); + }); + } + + [Test] + public async Task RemoveAsync_MissingPrerequisite_ReturnsFalse() + { + var repository = new InMemoryPrerequisiteRepository(); + + bool removed = await repository.RemoveAsync(new LabPrerequisite(_labA, _labB)); + + Assert.That(removed, Is.False); + } +} \ No newline at end of file diff --git a/backend/Platform.Prerequisites/Contracts/IPrerequisiteRepository.cs b/backend/Platform.Prerequisites/Contracts/IPrerequisiteRepository.cs new file mode 100644 index 0000000..32a145f --- /dev/null +++ b/backend/Platform.Prerequisites/Contracts/IPrerequisiteRepository.cs @@ -0,0 +1,18 @@ +using Platform.Prerequisites.Domain; + +namespace Platform.Prerequisites.Contracts; + +public interface IPrerequisiteRepository +{ + Task AddAsync( + LabPrerequisite prerequisite, + CancellationToken cancellationToken = default); + + Task RemoveAsync( + LabPrerequisite prerequisite, + CancellationToken cancellationToken = default); + + Task> GetForLaboratoryAsync( + Guid laboratoryId, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/backend/Platform.Prerequisites/Domain/AddPrerequisiteResult.cs b/backend/Platform.Prerequisites/Domain/AddPrerequisiteResult.cs new file mode 100644 index 0000000..791ac6e --- /dev/null +++ b/backend/Platform.Prerequisites/Domain/AddPrerequisiteResult.cs @@ -0,0 +1,8 @@ +namespace Platform.Prerequisites.Domain; + +public enum AddPrerequisiteResult +{ + Added, + Duplicate, + WouldCreateCycle +} \ No newline at end of file diff --git a/backend/Platform.Prerequisites/Infrastructure/InMemoryPrerequisiteRepository.cs b/backend/Platform.Prerequisites/Infrastructure/InMemoryPrerequisiteRepository.cs new file mode 100644 index 0000000..0b977a4 --- /dev/null +++ b/backend/Platform.Prerequisites/Infrastructure/InMemoryPrerequisiteRepository.cs @@ -0,0 +1,59 @@ +using Platform.Prerequisites.Contracts; +using Platform.Prerequisites.Domain; + +namespace Platform.Prerequisites.Infrastructure; + +public sealed class InMemoryPrerequisiteRepository : IPrerequisiteRepository +{ + private readonly List _prerequisites = new(); + private readonly object _lock = new(); + + public Task AddAsync( + LabPrerequisite prerequisite, + CancellationToken cancellationToken = default) + { + lock (_lock) + { + if (PrerequisiteRules.IsDuplicate(_prerequisites, prerequisite)) + { + return Task.FromResult(AddPrerequisiteResult.Duplicate); + } + + if (PrerequisiteRules.WouldCreateCycle(_prerequisites, prerequisite)) + { + return Task.FromResult(AddPrerequisiteResult.WouldCreateCycle); + } + + _prerequisites.Add(prerequisite); + return Task.FromResult(AddPrerequisiteResult.Added); + } + } + + public Task RemoveAsync( + LabPrerequisite prerequisite, + CancellationToken cancellationToken = default) + { + lock (_lock) + { + int removed = _prerequisites.RemoveAll(item => + item.LaboratoryId == prerequisite.LaboratoryId && + item.RequiredLaboratoryId == prerequisite.RequiredLaboratoryId); + + return Task.FromResult(removed > 0); + } + } + + public Task> GetForLaboratoryAsync( + Guid laboratoryId, + CancellationToken cancellationToken = default) + { + lock (_lock) + { + IReadOnlyList result = _prerequisites + .Where(item => item.LaboratoryId == laboratoryId) + .ToList(); + + return Task.FromResult(result); + } + } +} \ No newline at end of file