Skip to content

Commit d389fb3

Browse files
Copilotimnasnainaec
authored andcommitted
Add WordRepository integration tests
1 parent 0dc6255 commit d389fb3

File tree

6 files changed

+811
-3
lines changed

6 files changed

+811
-3
lines changed

.github/workflows/backend.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ jobs:
4141
dc.services.visualstudio.com:443
4242
deb.debian.org:80
4343
github.com:443
44-
md-hdd-t032zjxllntc.z26.blob.storage.azure.net:443
4544
objects.githubusercontent.com:443
4645
- name: Checkout repository
4746
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

Backend.Tests/Backend.Tests.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
<NoWarn>$(NoWarn);CA1305;CA1859;CS1591</NoWarn>
1414
</PropertyGroup>
1515
<ItemGroup>
16+
<PackageReference Include="EphemeralMongo7.runtime.linux-x64" Version="2.0.0" Condition="$([MSBuild]::IsOSPlatform('Linux'))" />
17+
<PackageReference Include="EphemeralMongo7.runtime.osx-arm64" Version="2.0.0" Condition="$([MSBuild]::IsOSPlatform('OSX'))" />
18+
<PackageReference Include="EphemeralMongo7.runtime.win-x64" Version="2.0.0" Condition="$([MSBuild]::IsOSPlatform('Windows'))" />
1619
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
1720
<PackageReference Include="Moq.Contrib.HttpClient" Version="1.4.0" />
1821
<PackageReference Include="NUnit" Version="4.4.0" />
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.IO;
4+
using System.Net.Sockets;
5+
using System.Runtime.InteropServices;
6+
using System.Threading;
7+
using MongoDB.Bson;
8+
using MongoDB.Driver;
9+
10+
namespace Backend.Tests.Repositories
11+
{
12+
/// <summary>
13+
/// Starts and manages an ephemeral MongoDB process for integration testing.
14+
/// Uses the mongod binary from the EphemeralMongo7 NuGet package.
15+
/// Supports single-node replica sets to enable multi-document transactions.
16+
/// </summary>
17+
internal sealed class MongoDbTestRunner : IDisposable
18+
{
19+
private const string Host = "127.0.0.1";
20+
private const string ReplicaSetName = "rs0";
21+
22+
private readonly Process _process;
23+
private readonly string _dataDirectory;
24+
25+
public string ConnectionString { get; }
26+
27+
private MongoDbTestRunner(Process process, string dataDirectory, string connectionString)
28+
{
29+
_process = process;
30+
_dataDirectory = dataDirectory;
31+
ConnectionString = connectionString;
32+
}
33+
34+
/// <summary>
35+
/// Starts a MongoDB instance as a single-node replica set.
36+
/// </summary>
37+
public static MongoDbTestRunner Start()
38+
{
39+
var binaryPath = FindMongodBinary();
40+
var port = FindFreePort();
41+
var dataDirectory = Path.Combine(Path.GetTempPath(), $"mongo-test-{Guid.NewGuid():N}");
42+
Directory.CreateDirectory(dataDirectory);
43+
44+
var process = StartMongodProcess(binaryPath, port, dataDirectory);
45+
try
46+
{
47+
WaitForMongoReady(port);
48+
InitializeReplicaSet(port);
49+
WaitForReplicaSetReady(port);
50+
}
51+
catch
52+
{
53+
process.Kill(entireProcessTree: true);
54+
process.Dispose();
55+
Directory.Delete(dataDirectory, recursive: true);
56+
throw;
57+
}
58+
59+
var connectionString = $"mongodb://{Host}:{port}/?directConnection=true&replicaSet={ReplicaSetName}";
60+
return new MongoDbTestRunner(process, dataDirectory, connectionString);
61+
}
62+
63+
private static string FindMongodBinary()
64+
{
65+
var rid = GetRuntimeId();
66+
var baseDir = AppContext.BaseDirectory;
67+
var binaryName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "mongod.exe" : "mongod";
68+
var binaryPath = Path.Combine(baseDir, "runtimes", rid, "native", "mongodb", "bin", binaryName);
69+
70+
if (!File.Exists(binaryPath))
71+
{
72+
throw new FileNotFoundException(
73+
$"mongod binary not found at '{binaryPath}'. Ensure one of the EphemeralMongo7.runtime.* packages is installed.",
74+
binaryPath);
75+
}
76+
77+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
78+
{
79+
// Ensure the binary is executable on Unix
80+
var chmod = Process.Start(new ProcessStartInfo("chmod")
81+
{
82+
ArgumentList = { "+x", binaryPath },
83+
UseShellExecute = false,
84+
});
85+
chmod?.WaitForExit();
86+
}
87+
88+
return binaryPath;
89+
}
90+
91+
private static string GetRuntimeId()
92+
{
93+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return "win-x64";
94+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "linux-x64";
95+
// EphemeralMongo7 only provides an arm64 binary for macOS
96+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return "osx-arm64";
97+
throw new PlatformNotSupportedException("Unsupported operating system.");
98+
}
99+
100+
private static int FindFreePort()
101+
{
102+
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
103+
socket.Bind(new System.Net.IPEndPoint(System.Net.IPAddress.Loopback, 0));
104+
return ((System.Net.IPEndPoint)socket.LocalEndPoint!).Port;
105+
}
106+
107+
private static Process StartMongodProcess(string binaryPath, int port, string dataDirectory)
108+
{
109+
var args = string.Join(" ",
110+
$"--replSet {ReplicaSetName}",
111+
$"--bind_ip {Host}",
112+
$"--port {port}",
113+
$"--dbpath \"{dataDirectory}\"",
114+
"--noauth",
115+
"--quiet");
116+
117+
var process = new Process
118+
{
119+
StartInfo = new ProcessStartInfo(binaryPath, args)
120+
{
121+
UseShellExecute = false,
122+
RedirectStandardOutput = true,
123+
RedirectStandardError = true,
124+
}
125+
};
126+
process.Start();
127+
process.BeginOutputReadLine();
128+
process.BeginErrorReadLine();
129+
return process;
130+
}
131+
132+
private static void WaitForMongoReady(int port, int timeoutSeconds = 30)
133+
{
134+
var deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds);
135+
while (DateTime.UtcNow < deadline)
136+
{
137+
try
138+
{
139+
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
140+
socket.Connect(Host, port);
141+
return;
142+
}
143+
catch (SocketException)
144+
{
145+
Thread.Sleep(200);
146+
}
147+
}
148+
149+
throw new TimeoutException($"MongoDB did not start within {timeoutSeconds} seconds on port {port}.");
150+
}
151+
152+
private static void InitializeReplicaSet(int port)
153+
{
154+
var client = new MongoClient($"mongodb://{Host}:{port}/?directConnection=true");
155+
var admin = client.GetDatabase("admin");
156+
var config = new BsonDocument
157+
{
158+
{ "_id", ReplicaSetName },
159+
{ "members", new BsonArray { new BsonDocument { { "_id", 0 }, { "host", $"{Host}:{port}" } } } }
160+
};
161+
admin.RunCommand<BsonDocument>(new BsonDocument("replSetInitiate", config));
162+
}
163+
164+
private static void WaitForReplicaSetReady(int port, int timeoutSeconds = 30)
165+
{
166+
var client = new MongoClient(
167+
$"mongodb://{Host}:{port}/?directConnection=true&replicaSet={ReplicaSetName}");
168+
var deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds);
169+
Exception? lastException = null;
170+
while (DateTime.UtcNow < deadline)
171+
{
172+
try
173+
{
174+
var admin = client.GetDatabase("admin");
175+
var status = admin.RunCommand<BsonDocument>(new BsonDocument("replSetGetStatus", 1));
176+
if (status["ok"].ToInt32() == 1 && status["myState"].ToInt32() == 1)
177+
{
178+
return;
179+
}
180+
}
181+
catch (Exception ex)
182+
{
183+
lastException = ex;
184+
}
185+
186+
Thread.Sleep(500);
187+
}
188+
189+
throw new TimeoutException(
190+
$"Replica set did not become ready within {timeoutSeconds} seconds.", lastException);
191+
}
192+
193+
public void Dispose()
194+
{
195+
try
196+
{
197+
if (!_process.HasExited)
198+
{
199+
_process.Kill(entireProcessTree: true);
200+
}
201+
}
202+
catch (InvalidOperationException) { }
203+
204+
_process.Dispose();
205+
206+
try
207+
{
208+
Directory.Delete(_dataDirectory, recursive: true);
209+
}
210+
catch (IOException) { }
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)