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.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.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.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.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/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/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 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 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 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 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 @@ + +