From 8714636060ef524614dd59442faa82658d43c83e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 7 Mar 2026 12:32:48 +0000
Subject: [PATCH 1/3] Initial plan
From ef57d4bc0bb87237c39ba569b7522475983d9192 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 7 Mar 2026 13:11:20 +0000
Subject: [PATCH 2/3] Fix MongoDB.Driver 3.4.0 compilation errors in test mocks
- Add 'using MongoDB.Driver.Search' for IMongoSearchIndexManager
- Add InsertOneAsync(T, CancellationToken) overload forwarding token
- Add ReplaceOne/ReplaceOneAsync overloads accepting UpdateOptions
- Wrap MapReduce methods in #pragma warning disable/restore CS0618
- Add DropCollection/DropCollectionAsync overloads with DropCollectionOptions+name
- Fix missing 'using MongoDB.Driver' in WordRepositoryTestHelper
- Fix using directive issues in WordRepositoryTests (swap Generic for System)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Mocks/InMemoryMongoCollection.cs | 901 ++++++++++++++++++
Backend.Tests/Mocks/InMemoryMongoDatabase.cs | 343 +++++++
.../Mocks/WordRepositoryTestHelper.cs | 162 ++++
.../Repositories/WordRepositoryTests.cs | 418 ++++++++
4 files changed, 1824 insertions(+)
create mode 100644 Backend.Tests/Mocks/InMemoryMongoCollection.cs
create mode 100644 Backend.Tests/Mocks/InMemoryMongoDatabase.cs
create mode 100644 Backend.Tests/Mocks/WordRepositoryTestHelper.cs
create mode 100644 Backend.Tests/Repositories/WordRepositoryTests.cs
diff --git a/Backend.Tests/Mocks/InMemoryMongoCollection.cs b/Backend.Tests/Mocks/InMemoryMongoCollection.cs
new file mode 100644
index 0000000000..221b7425fd
--- /dev/null
+++ b/Backend.Tests/Mocks/InMemoryMongoCollection.cs
@@ -0,0 +1,901 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using MongoDB.Bson;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.Serialization;
+using MongoDB.Driver;
+using MongoDB.Driver.Search;
+
+namespace Backend.Tests.Mocks;
+
+///
+/// An in-memory implementation of for testing.
+/// Supports the operations used by .
+///
+internal sealed class InMemoryMongoCollection : IMongoCollection
+{
+ private readonly List _documents = [];
+ private readonly IBsonSerializer _serializer;
+ private readonly IBsonSerializerRegistry _registry;
+
+ public InMemoryMongoCollection(string collectionName)
+ {
+ _serializer = BsonSerializer.LookupSerializer();
+ _registry = BsonSerializer.SerializerRegistry;
+ CollectionNamespace = new CollectionNamespace("testDb", collectionName);
+ }
+
+ public CollectionNamespace CollectionNamespace { get; }
+ public IMongoDatabase Database => throw new NotSupportedException();
+ public IBsonSerializer DocumentSerializer => _serializer;
+ public IMongoIndexManager Indexes => throw new NotSupportedException();
+ public IMongoSearchIndexManager SearchIndexes => throw new NotSupportedException();
+ public MongoCollectionSettings Settings => new MongoCollectionSettings();
+
+ // --- Core methods used by WordRepository ---
+
+ public Task> FindAsync(
+ FilterDefinition filter,
+ FindOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => Task.FromResult(BuildCursor(filter, options?.Limit));
+
+ public Task> FindAsync(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ FindOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => FindAsync(filter, options, cancellationToken);
+
+ public IAsyncCursor FindSync(
+ FilterDefinition filter,
+ FindOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => BuildCursor(filter, options?.Limit);
+
+ public IAsyncCursor FindSync(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ FindOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => FindSync(filter, options, cancellationToken);
+
+ public Task InsertManyAsync(
+ IEnumerable documents,
+ InsertManyOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ DoInsertMany(documents);
+ return Task.CompletedTask;
+ }
+
+ public Task InsertManyAsync(
+ IClientSessionHandle session,
+ IEnumerable documents,
+ InsertManyOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => InsertManyAsync(documents, options, cancellationToken);
+
+ public Task InsertOneAsync(
+ T document,
+ InsertOneOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ DoInsertMany([document]);
+ return Task.CompletedTask;
+ }
+
+ public Task InsertOneAsync(
+ T document,
+ CancellationToken cancellationToken)
+ => InsertOneAsync(document, null, cancellationToken);
+
+ public Task InsertOneAsync(
+ IClientSessionHandle session,
+ T document,
+ InsertOneOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => InsertOneAsync(document, options, cancellationToken);
+
+ public Task FindOneAndDeleteAsync(
+ FilterDefinition filter,
+ FindOneAndDeleteOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ var renderArgs = new RenderArgs(_serializer, _registry);
+ var renderedFilter = filter.Render(renderArgs);
+ var doc = _documents.FirstOrDefault(d => MatchesFilter(d, renderedFilter));
+ if (doc is null)
+ {
+ return Task.FromResult(default!);
+ }
+
+ _documents.Remove(doc);
+ return Task.FromResult(BsonSerializer.Deserialize(doc));
+ }
+
+ public Task FindOneAndDeleteAsync(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ FindOneAndDeleteOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => FindOneAndDeleteAsync(filter, options, cancellationToken);
+
+ public Task DeleteManyAsync(
+ FilterDefinition filter,
+ CancellationToken cancellationToken = default)
+ => DeleteManyAsync(filter, null, cancellationToken);
+
+ public Task DeleteManyAsync(
+ FilterDefinition filter,
+ DeleteOptions? options,
+ CancellationToken cancellationToken = default)
+ {
+ var renderArgs = new RenderArgs(_serializer, _registry);
+ var renderedFilter = filter.Render(renderArgs);
+ var toRemove = _documents.Where(d => MatchesFilter(d, renderedFilter)).ToList();
+ toRemove.ForEach(d => _documents.Remove(d));
+ return Task.FromResult(new DeleteResult.Acknowledged(toRemove.Count));
+ }
+
+ public Task CountDocumentsAsync(
+ FilterDefinition filter,
+ CountOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ var renderArgs = new RenderArgs(_serializer, _registry);
+ var renderedFilter = filter.Render(renderArgs);
+ var count = _documents.Count(d => MatchesFilter(d, renderedFilter));
+ if (options?.Limit.HasValue == true)
+ {
+ count = Math.Min(count, (int)options.Limit.Value);
+ }
+
+ return Task.FromResult((long)count);
+ }
+
+ // --- Private helpers ---
+
+ private void DoInsertMany(IEnumerable documents)
+ {
+ foreach (var document in documents)
+ {
+ EnsureId(document);
+ var bsonDoc = SerializeDocument(document);
+ _documents.Add(bsonDoc);
+ }
+ }
+
+ private static void EnsureId(T document)
+ {
+ var idProp = typeof(T).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance);
+ if (idProp?.PropertyType == typeof(string) && idProp.GetValue(document) is string id &&
+ string.IsNullOrEmpty(id))
+ {
+ idProp.SetValue(document, ObjectId.GenerateNewId().ToString());
+ }
+ }
+
+ private BsonDocument SerializeDocument(T document)
+ {
+ using var writer = new BsonDocumentWriter(new BsonDocument());
+ var context = BsonSerializationContext.CreateRoot(writer);
+ _serializer.Serialize(context, document);
+ return writer.Document;
+ }
+
+ private IEnumerable GetMatchingDocuments(FilterDefinition filter, int? limit)
+ {
+ var renderArgs = new RenderArgs(_serializer, _registry);
+ var renderedFilter = filter.Render(renderArgs);
+ var matching = _documents.Where(d => MatchesFilter(d, renderedFilter));
+ if (limit.HasValue)
+ {
+ matching = matching.Take(limit.Value);
+ }
+
+ return matching.Select(doc => BsonSerializer.Deserialize(doc));
+ }
+
+ private IAsyncCursor BuildCursor(FilterDefinition filter, int? limit)
+ {
+ var documents = GetMatchingDocuments(filter, limit);
+ if (typeof(TProjection) != typeof(T))
+ {
+ throw new NotSupportedException("Projection to a different type is not supported in InMemoryMongoCollection");
+ }
+
+ return new InMemoryAsyncCursor(documents.Cast());
+ }
+
+ // --- BSON filter evaluator ---
+
+ private static bool MatchesFilter(BsonDocument doc, BsonDocument filter)
+ {
+ foreach (var element in filter)
+ {
+ switch (element.Name)
+ {
+ case "$and":
+ if (!element.Value.AsBsonArray.All(f => MatchesFilter(doc, f.AsBsonDocument)))
+ {
+ return false;
+ }
+
+ break;
+ default:
+ var fieldValue = GetFieldValue(doc, element.Name);
+ if (!MatchesFieldValue(fieldValue, element.Value))
+ {
+ return false;
+ }
+
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ private static BsonValue GetFieldValue(BsonDocument doc, string fieldPath)
+ {
+ var parts = fieldPath.Split('.');
+ BsonValue current = doc;
+ foreach (var part in parts)
+ {
+ if (current is BsonDocument bsonDoc)
+ {
+ if (!bsonDoc.TryGetValue(part, out current))
+ {
+ return BsonNull.Value;
+ }
+ }
+ else if (current is BsonArray bsonArr)
+ {
+ if (int.TryParse(part, out var idx) && idx < bsonArr.Count)
+ {
+ current = bsonArr[idx];
+ }
+ else
+ {
+ return BsonNull.Value;
+ }
+ }
+ else
+ {
+ return BsonNull.Value;
+ }
+ }
+
+ return current;
+ }
+
+ private static bool MatchesFieldValue(BsonValue docValue, BsonValue filterValue)
+ {
+ if (filterValue is BsonDocument operators)
+ {
+ foreach (var op in operators)
+ {
+ switch (op.Name)
+ {
+ case "$in":
+ if (!op.Value.AsBsonArray.Contains(docValue))
+ {
+ return false;
+ }
+
+ break;
+ case "$exists":
+ var shouldExist = op.Value.AsBoolean;
+ var exists = docValue is not BsonNull && docValue != BsonNull.Value;
+ if (shouldExist != exists)
+ {
+ return false;
+ }
+
+ break;
+ case "$elemMatch":
+ if (docValue is not BsonArray arr || !arr.Any(
+ elem => elem is BsonDocument elemDoc && MatchesFilter(elemDoc, op.Value.AsBsonDocument)))
+ {
+ return false;
+ }
+
+ break;
+ default:
+ throw new NotSupportedException(
+ $"Filter operator '{op.Name}' is not supported by InMemoryMongoCollection");
+ }
+ }
+
+ return true;
+ }
+
+ // Simple equality
+ return docValue.Equals(filterValue);
+ }
+
+ // --- Unsupported IMongoCollection methods ---
+
+ public IAsyncCursor Aggregate(
+ PipelineDefinition pipeline,
+ AggregateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public IAsyncCursor Aggregate(
+ IClientSessionHandle session,
+ PipelineDefinition pipeline,
+ AggregateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task> AggregateAsync(
+ PipelineDefinition pipeline,
+ AggregateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task> AggregateAsync(
+ IClientSessionHandle session,
+ PipelineDefinition pipeline,
+ AggregateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public void AggregateToCollection(
+ PipelineDefinition pipeline,
+ AggregateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public void AggregateToCollection(
+ IClientSessionHandle session,
+ PipelineDefinition pipeline,
+ AggregateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task AggregateToCollectionAsync(
+ PipelineDefinition pipeline,
+ AggregateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task AggregateToCollectionAsync(
+ IClientSessionHandle session,
+ PipelineDefinition pipeline,
+ AggregateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public BulkWriteResult BulkWrite(
+ IEnumerable> requests,
+ BulkWriteOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public BulkWriteResult BulkWrite(
+ IClientSessionHandle session,
+ IEnumerable> requests,
+ BulkWriteOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task> BulkWriteAsync(
+ IEnumerable> requests,
+ BulkWriteOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task> BulkWriteAsync(
+ IClientSessionHandle session,
+ IEnumerable> requests,
+ BulkWriteOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ [Obsolete]
+ public long Count(
+ FilterDefinition filter,
+ CountOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ [Obsolete]
+ public long Count(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ CountOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ [Obsolete]
+ public Task CountAsync(
+ FilterDefinition filter,
+ CountOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ [Obsolete]
+ public Task CountAsync(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ CountOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public long CountDocuments(
+ FilterDefinition filter,
+ CountOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public long CountDocuments(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ CountOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task CountDocumentsAsync(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ CountOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => CountDocumentsAsync(filter, options, cancellationToken);
+
+ public DeleteResult DeleteMany(
+ FilterDefinition filter,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public DeleteResult DeleteMany(
+ FilterDefinition filter,
+ DeleteOptions options,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public DeleteResult DeleteMany(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ DeleteOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task DeleteManyAsync(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ DeleteOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => DeleteManyAsync(filter, options, cancellationToken);
+
+ public DeleteResult DeleteOne(
+ FilterDefinition filter,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public DeleteResult DeleteOne(
+ FilterDefinition filter,
+ DeleteOptions? options,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public DeleteResult DeleteOne(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ DeleteOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task DeleteOneAsync(
+ FilterDefinition filter,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task DeleteOneAsync(
+ FilterDefinition filter,
+ DeleteOptions? options,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task DeleteOneAsync(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ DeleteOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public IAsyncCursor Distinct(
+ FieldDefinition field,
+ FilterDefinition filter,
+ DistinctOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public IAsyncCursor Distinct(
+ IClientSessionHandle session,
+ FieldDefinition field,
+ FilterDefinition filter,
+ DistinctOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task> DistinctAsync(
+ FieldDefinition field,
+ FilterDefinition filter,
+ DistinctOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task> DistinctAsync(
+ IClientSessionHandle session,
+ FieldDefinition field,
+ FilterDefinition filter,
+ DistinctOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public IAsyncCursor DistinctMany(
+ FieldDefinition> field,
+ FilterDefinition filter,
+ DistinctOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public IAsyncCursor DistinctMany(
+ IClientSessionHandle session,
+ FieldDefinition> field,
+ FilterDefinition filter,
+ DistinctOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task> DistinctManyAsync(
+ FieldDefinition> field,
+ FilterDefinition filter,
+ DistinctOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task> DistinctManyAsync(
+ IClientSessionHandle session,
+ FieldDefinition> field,
+ FilterDefinition filter,
+ DistinctOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public long EstimatedDocumentCount(
+ EstimatedDocumentCountOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => _documents.Count;
+
+ public Task EstimatedDocumentCountAsync(
+ EstimatedDocumentCountOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => Task.FromResult((long)_documents.Count);
+
+ public TProjection FindOneAndDelete(
+ FilterDefinition filter,
+ FindOneAndDeleteOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public TProjection FindOneAndDelete(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ FindOneAndDeleteOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public TProjection FindOneAndReplace(
+ FilterDefinition filter,
+ T replacement,
+ FindOneAndReplaceOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public TProjection FindOneAndReplace(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ T replacement,
+ FindOneAndReplaceOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task FindOneAndReplaceAsync(
+ FilterDefinition filter,
+ T replacement,
+ FindOneAndReplaceOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task FindOneAndReplaceAsync(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ T replacement,
+ FindOneAndReplaceOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public TProjection FindOneAndUpdate(
+ FilterDefinition filter,
+ UpdateDefinition update,
+ FindOneAndUpdateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public TProjection FindOneAndUpdate(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ UpdateDefinition update,
+ FindOneAndUpdateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task FindOneAndUpdateAsync(
+ FilterDefinition filter,
+ UpdateDefinition update,
+ FindOneAndUpdateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task FindOneAndUpdateAsync(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ UpdateDefinition update,
+ FindOneAndUpdateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public void InsertMany(
+ IEnumerable documents,
+ InsertManyOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => DoInsertMany(documents);
+
+ public void InsertMany(
+ IClientSessionHandle session,
+ IEnumerable documents,
+ InsertManyOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => InsertMany(documents, options, cancellationToken);
+
+ public void InsertOne(
+ T document,
+ InsertOneOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => DoInsertMany([document]);
+
+ public void InsertOne(
+ IClientSessionHandle session,
+ T document,
+ InsertOneOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => InsertOne(document, options, cancellationToken);
+
+#pragma warning disable CS0618
+ public IAsyncCursor MapReduce(
+ BsonJavaScript map,
+ BsonJavaScript reduce,
+ MapReduceOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public IAsyncCursor MapReduce(
+ IClientSessionHandle session,
+ BsonJavaScript map,
+ BsonJavaScript reduce,
+ MapReduceOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task> MapReduceAsync(
+ BsonJavaScript map,
+ BsonJavaScript reduce,
+ MapReduceOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task> MapReduceAsync(
+ IClientSessionHandle session,
+ BsonJavaScript map,
+ BsonJavaScript reduce,
+ MapReduceOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+#pragma warning restore CS0618
+
+ public IFilteredMongoCollection OfType()
+ where TDerivedDocument : T
+ => throw new NotSupportedException();
+
+ public ReplaceOneResult ReplaceOne(
+ FilterDefinition filter,
+ T replacement,
+ ReplaceOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public ReplaceOneResult ReplaceOne(
+ FilterDefinition filter,
+ T replacement,
+ UpdateOptions options,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public ReplaceOneResult ReplaceOne(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ T replacement,
+ ReplaceOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public ReplaceOneResult ReplaceOne(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ T replacement,
+ UpdateOptions options,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task ReplaceOneAsync(
+ FilterDefinition filter,
+ T replacement,
+ ReplaceOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task ReplaceOneAsync(
+ FilterDefinition filter,
+ T replacement,
+ UpdateOptions options,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task ReplaceOneAsync(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ T replacement,
+ ReplaceOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task ReplaceOneAsync(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ T replacement,
+ UpdateOptions options,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public UpdateResult UpdateMany(
+ FilterDefinition filter,
+ UpdateDefinition update,
+ UpdateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public UpdateResult UpdateMany(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ UpdateDefinition update,
+ UpdateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task UpdateManyAsync(
+ FilterDefinition filter,
+ UpdateDefinition update,
+ UpdateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task UpdateManyAsync(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ UpdateDefinition update,
+ UpdateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public UpdateResult UpdateOne(
+ FilterDefinition filter,
+ UpdateDefinition update,
+ UpdateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public UpdateResult UpdateOne(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ UpdateDefinition update,
+ UpdateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task UpdateOneAsync(
+ FilterDefinition filter,
+ UpdateDefinition update,
+ UpdateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task UpdateOneAsync(
+ IClientSessionHandle session,
+ FilterDefinition filter,
+ UpdateDefinition update,
+ UpdateOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public IChangeStreamCursor Watch(
+ PipelineDefinition, TResult> pipeline,
+ ChangeStreamOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public IChangeStreamCursor Watch(
+ IClientSessionHandle session,
+ PipelineDefinition, TResult> pipeline,
+ ChangeStreamOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task> WatchAsync(
+ PipelineDefinition, TResult> pipeline,
+ ChangeStreamOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public Task> WatchAsync(
+ IClientSessionHandle session,
+ PipelineDefinition, TResult> pipeline,
+ ChangeStreamOptions? options = null,
+ CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public IMongoCollection WithReadConcern(ReadConcern readConcern) => this;
+ public IMongoCollection WithReadPreference(ReadPreference readPreference) => this;
+ public IMongoCollection WithWriteConcern(WriteConcern writeConcern) => this;
+}
+
+///
+/// A simple in-memory that yields all items in a single batch.
+///
+internal sealed class InMemoryAsyncCursor(IEnumerable documents) : IAsyncCursor
+{
+ private readonly IEnumerator _enumerator = documents.GetEnumerator();
+ private bool _exhausted;
+
+ public IEnumerable Current { get; private set; } = [];
+
+ public bool MoveNext(CancellationToken cancellationToken = default)
+ {
+ if (_exhausted)
+ {
+ return false;
+ }
+
+ _exhausted = true;
+ var batch = new List();
+ while (_enumerator.MoveNext())
+ {
+ batch.Add(_enumerator.Current);
+ }
+
+ Current = batch;
+ return batch.Count > 0;
+ }
+
+ public Task MoveNextAsync(CancellationToken cancellationToken = default)
+ => Task.FromResult(MoveNext(cancellationToken));
+
+ public void Dispose() => _enumerator.Dispose();
+}
diff --git a/Backend.Tests/Mocks/InMemoryMongoDatabase.cs b/Backend.Tests/Mocks/InMemoryMongoDatabase.cs
new file mode 100644
index 0000000000..d62b339af4
--- /dev/null
+++ b/Backend.Tests/Mocks/InMemoryMongoDatabase.cs
@@ -0,0 +1,343 @@
+using System;
+using System.Collections.Concurrent;
+using System.Threading;
+using System.Threading.Tasks;
+using MongoDB.Bson;
+using MongoDB.Driver;
+
+namespace Backend.Tests.Mocks;
+
+///
+/// An in-memory implementation of for testing.
+/// Returns instances keyed by collection name.
+///
+internal sealed class InMemoryMongoDatabase : IMongoDatabase
+{
+ private readonly ConcurrentDictionary _collections = new();
+
+ public IMongoCollection GetCollection(
+ string name,
+ MongoCollectionSettings? settings = null)
+ {
+ return (IMongoCollection)_collections.GetOrAdd(
+ name,
+ _ => new InMemoryMongoCollection