Skip to content
Draft
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: 6 additions & 1 deletion Backend.Tests/Controllers/WordControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace Backend.Tests.Controllers
internal sealed class WordControllerTests : IDisposable
{
private WordRepositoryMock _wordRepo = null!;
private SemanticDomainCountRepositoryMock _semDomCountRepo = null!;
private IPermissionService _permissionService = null!;
private IWordService _wordService = null!;
private WordController _wordController = null!;
Expand All @@ -32,9 +33,10 @@ public void Dispose()
public void Setup()
{
_wordRepo = new WordRepositoryMock();
_semDomCountRepo = new SemanticDomainCountRepositoryMock();
_wordService = new WordService(_wordRepo);
_permissionService = new PermissionServiceMock();
_wordController = new WordController(_wordRepo, _wordService, _permissionService);
_wordController = new WordController(_wordRepo, _wordService, _semDomCountRepo, _permissionService);
}

[Test]
Expand Down Expand Up @@ -464,8 +466,11 @@ public async Task TestGetDomainWordCountNoPermission()
[Test]
public async Task TestGetDomainWordCount()
{
_semDomCountRepo.SetCount(ProjId, "1", 3);

var result = await _wordController.GetDomainWordCount(ProjId, "1");
Assert.That(result, Is.InstanceOf<OkObjectResult>());
Assert.That(((OkObjectResult)result).Value, Is.EqualTo(3));
}
}
}
71 changes: 71 additions & 0 deletions Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BackendFramework.Interfaces;
using BackendFramework.Models;
using MongoDB.Driver;

namespace Backend.Tests.Mocks
{
internal sealed class SemanticDomainCountRepositoryMock : ISemanticDomainCountRepository
{
private readonly List<ProjectSemanticDomainCount> _counts = [];

public Task<int> GetCount(string projectId, string domainId)
{
var count = _counts
.FirstOrDefault(c => c.ProjectId == projectId && c.DomainId == domainId)?.Count ?? 0;
return Task.FromResult(count);
}

public Task<List<ProjectSemanticDomainCount>> GetAllCounts(string projectId)
{
return Task.FromResult(_counts.Where(c => c.ProjectId == projectId).Select(c => c.Clone()).ToList());
}

// The session is ignored: this mock does not simulate transactions.
public Task ApplyDeltas(
IClientSessionHandle session, string projectId, IReadOnlyDictionary<string, int> domainDeltas)
{
foreach (var (domainId, delta) in domainDeltas)
{
if (delta == 0)
{
continue;
}

var existing = _counts.FirstOrDefault(c => c.ProjectId == projectId && c.DomainId == domainId);
if (existing is null)
{
_counts.Add(new ProjectSemanticDomainCount(projectId, domainId, delta));
}
else
{
existing.Count += delta;
}
}

return Task.CompletedTask;
}

public Task DeleteAllCounts(IClientSessionHandle session, string projectId)
{
_counts.RemoveAll(c => c.ProjectId == projectId);
return Task.CompletedTask;
}

/// <summary> Test helper to seed a count directly, without a transaction. </summary>
public void SetCount(string projectId, string domainId, int count)
{
var existing = _counts.FirstOrDefault(c => c.ProjectId == projectId && c.DomainId == domainId);
if (existing is null)
{
_counts.Add(new ProjectSemanticDomainCount(projectId, domainId, count));
}
else
{
existing.Count = count;
}
}
}
}
7 changes: 0 additions & 7 deletions Backend.Tests/Mocks/WordRepositoryMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,12 +306,5 @@ public async Task<bool> RevertReplaceFrontier(

return true;
}

public Task<int> CountFrontierWordsWithDomain(string projectId, string domainId)
{
var count = _frontier.Count(
w => w.ProjectId == projectId && w.Senses.Any(s => s.SemanticDomains.Any(sd => sd.Id == domainId)));
return Task.FromResult(count);
}
}
}
26 changes: 26 additions & 0 deletions Backend.Tests/Repositories/MongoDbSetUpFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using NUnit.Framework;

namespace Backend.Tests.Repositories
{
/// <summary>
/// Starts a single shared MongoDB instance for all repository integration tests.
/// Fixtures isolate themselves by using distinct database names.
/// </summary>
[SetUpFixture]
public sealed class MongoDbSetUpFixture
{
internal static MongoDbTestRunner Runner { get; private set; } = null!;

[OneTimeSetUp]
public void StartMongo()
{
Runner = MongoDbTestRunner.Start();
}

[OneTimeTearDown]
public void StopMongo()
{
Runner?.Dispose();
}
}
}
9 changes: 8 additions & 1 deletion Backend.Tests/Repositories/MongoDbTestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,16 @@ private static void WaitForReplicaSetReady(int port, int timeoutSeconds = 30)
{
var admin = client.GetDatabase("admin");
var status = admin.RunCommand<BsonDocument>(new BsonDocument("replSetGetStatus", 1));
// myState == 1 means PRIMARY, but a freshly elected primary briefly rejects writes with
// "not primary" until it finishes transitioning. Also confirm it is writable via hello so
// callers that write immediately (e.g. index creation) don't race that window.
if (status["ok"].ToInt32() == 1 && status["myState"].ToInt32() == 1)
{
return;
var hello = admin.RunCommand<BsonDocument>(new BsonDocument("hello", 1));
if (hello.GetValue("isWritablePrimary", BsonBoolean.False).ToBoolean())
{
return;
}
}
}
catch (Exception ex)
Expand Down
135 changes: 135 additions & 0 deletions Backend.Tests/Repositories/SemanticDomainCountRepositoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BackendFramework.Contexts;
using BackendFramework.Models;
using BackendFramework.Repositories;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using NUnit.Framework;

namespace Backend.Tests.Repositories
{
/// <summary>
/// Integration tests for <see cref="SemanticDomainCountRepository"/> that spin up an actual MongoDB instance.
/// A single-node replica set is required because the write methods run inside transactions.
/// </summary>
[TestFixture]
[Category("IntegrationTest")]
public sealed class SemanticDomainCountRepositoryTests
{
private MongoDbContext _dbContext = null!;
private SemanticDomainCountRepository _repo = null!;
private string _projectId = null!;

[SetUp]
public void SetUp()
{
_projectId = Guid.NewGuid().ToString();
var options = Options.Create(new BackendFramework.Startup.Settings
{
ConnectionString = MongoDbSetUpFixture.Runner.ConnectionString,
CombineDatabase = "SemanticDomainCountRepositoryTests",
});
_dbContext = new MongoDbContext(options);
_repo = new SemanticDomainCountRepository(_dbContext);
}

private Task ApplyDeltas(IReadOnlyDictionary<string, int> deltas)
{
return _dbContext.ExecuteInTransaction(async session =>
{
await _repo.ApplyDeltas(session, _projectId, deltas);
return true;
});
}

[Test]
public async Task ApplyDeltasUpsertsThenIncrements()
{
await ApplyDeltas(new Dictionary<string, int> { ["1"] = 2, ["1.1"] = 1 });
Assert.That(await _repo.GetCount(_projectId, "1"), Is.EqualTo(2));
Assert.That(await _repo.GetCount(_projectId, "1.1"), Is.EqualTo(1));

await ApplyDeltas(new Dictionary<string, int> { ["1"] = 3 });
Assert.That(await _repo.GetCount(_projectId, "1"), Is.EqualTo(5));
}

[Test]
public async Task ApplyDeltasDecrements()
{
await ApplyDeltas(new Dictionary<string, int> { ["1"] = 5 });
await ApplyDeltas(new Dictionary<string, int> { ["1"] = -2 });
Assert.That(await _repo.GetCount(_projectId, "1"), Is.EqualTo(3));
}

[Test]
public async Task ApplyDeltasSkipsZeroDeltas()
{
await ApplyDeltas(new Dictionary<string, int> { ["1"] = 0 });
Assert.That(await _repo.GetAllCounts(_projectId), Is.Empty);
}

[Test]
public async Task GetCountReturnsZeroWhenAbsent()
{
Assert.That(await _repo.GetCount(_projectId, "9.9"), Is.EqualTo(0));
}

[Test]
public async Task GetAllCountsReturnsProjectCounts()
{
await ApplyDeltas(new Dictionary<string, int> { ["1"] = 1, ["2"] = 4 });
var all = await _repo.GetAllCounts(_projectId);
Assert.That(all, Has.Count.EqualTo(2));
Assert.That(all.Find(c => c.DomainId == "2")!.Count, Is.EqualTo(4));
}

[Test]
public async Task GetAllCountsIsolatesByProject()
{
await ApplyDeltas(new Dictionary<string, int> { ["1"] = 1 });
Assert.That(await _repo.GetAllCounts(Guid.NewGuid().ToString()), Is.Empty);
}

[Test]
public async Task DeleteAllCountsRemovesProjectCounts()
{
await ApplyDeltas(new Dictionary<string, int> { ["1"] = 1, ["2"] = 2 });
await _dbContext.ExecuteInTransaction(async session =>
{
await _repo.DeleteAllCounts(session, _projectId);
return true;
});
Assert.That(await _repo.GetAllCounts(_projectId), Is.Empty);
}

[Test]
public void UniqueIndexPreventsDuplicatePairs()
{
var collection =
_dbContext.Db.GetCollection<ProjectSemanticDomainCount>("SemanticDomainCountCollection");
collection.InsertOne(new ProjectSemanticDomainCount(_projectId, "1", 1));
Assert.That(
async () => await collection.InsertOneAsync(new ProjectSemanticDomainCount(_projectId, "1", 1)),
Throws.InstanceOf<MongoWriteException>());
}

[Test]
public void CountDomainsTalliesEachSenseOccurrence()
{
var word = new Word
{
Senses =
[
new Sense { SemanticDomains = [new SemanticDomain { Id = "1" }, new SemanticDomain { Id = "2" }] },
new Sense { SemanticDomains = [new SemanticDomain { Id = "1" }] },
],
};

var counts = SemanticDomainCountRepository.CountDomains(word);
Assert.That(counts["1"], Is.EqualTo(2));
Assert.That(counts["2"], Is.EqualTo(1));
}
}
}
Loading
Loading