Skip to content

Commit 0772b0c

Browse files
Increase the test coverage
1 parent 703695f commit 0772b0c

File tree

2 files changed

+305
-0
lines changed

2 files changed

+305
-0
lines changed

unittests/nexus_engine/nexus_engine_extensions_unittests/Core/ManagerTests.cs

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,228 @@ public void Operations_AfterDispose_DoNotThrow()
306306
_ = m_Manager.ExtensionExists("test");
307307
_ = m_Manager.GetExtensionsVersion();
308308
}
309+
310+
/// <summary>
311+
/// Verifies that constructor handles directory creation failure gracefully.
312+
/// </summary>
313+
[Fact]
314+
public void Constructor_WhenDirectoryCreationFails_HandlesGracefully()
315+
{
316+
// Arrange
317+
_ = m_FileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(false);
318+
_ = m_FileSystemMock.Setup(fs => fs.CreateDirectory(It.IsAny<string>()));
319+
_ = m_FileSystemMock.SetupSequence(fs => fs.DirectoryExists(It.IsAny<string>()))
320+
.Returns(false) // First check - doesn't exist
321+
.Returns(false); // After creation - still doesn't exist (creation failed)
322+
323+
// Act - should not throw
324+
m_Manager = new Manager(m_FileSystemMock.Object, m_Settings.Object);
325+
326+
// Assert
327+
_ = m_Manager.Should().NotBeNull();
328+
}
329+
330+
/// <summary>
331+
/// Verifies that ValidateExtension returns invalid when script file does not exist.
332+
/// </summary>
333+
[Fact]
334+
public void ValidateExtension_WhenScriptFileDoesNotExist_ReturnsInvalid()
335+
{
336+
// Arrange
337+
m_Manager = new Manager(m_FileSystemMock.Object, m_Settings.Object);
338+
var extensionName = "TestExtension";
339+
var metadata = new Nexus.Engine.Extensions.Models.ExtensionMetadata
340+
{
341+
Name = extensionName,
342+
ScriptFile = "script.ps1",
343+
ScriptType = "powershell",
344+
FullScriptPath = "C:\\extensions\\TestExtension\\script.ps1",
345+
};
346+
347+
// Manually add extension to manager's internal dictionary via reflection
348+
var extensionsField = typeof(Manager).GetField("m_Extensions", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
349+
if (extensionsField?.GetValue(m_Manager) is System.Collections.Generic.Dictionary<string, Nexus.Engine.Extensions.Models.ExtensionMetadata> extensions)
350+
{
351+
extensions[extensionName] = metadata;
352+
}
353+
354+
// Setup file system to return false for file exists check
355+
_ = m_FileSystemMock.Setup(fs => fs.FileExists(It.IsAny<string>())).Returns(false);
356+
357+
// Act
358+
var (isValid, errorMessage) = m_Manager.ValidateExtension(extensionName);
359+
360+
// Assert
361+
_ = isValid.Should().BeFalse();
362+
_ = errorMessage.Should().Contain("not found");
363+
}
364+
365+
/// <summary>
366+
/// Verifies that ValidateExtension returns invalid when script file is null or empty.
367+
/// </summary>
368+
[Fact]
369+
public void ValidateExtension_WhenScriptFileIsEmpty_ReturnsInvalid()
370+
{
371+
// Arrange
372+
m_Manager = new Manager(m_FileSystemMock.Object, m_Settings.Object);
373+
var extensionName = "TestExtension";
374+
var metadata = new Nexus.Engine.Extensions.Models.ExtensionMetadata
375+
{
376+
Name = extensionName,
377+
ScriptFile = string.Empty,
378+
ScriptType = "powershell",
379+
};
380+
381+
// Manually add extension to manager's internal dictionary via reflection
382+
var extensionsField = typeof(Manager).GetField("m_Extensions", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
383+
if (extensionsField?.GetValue(m_Manager) is System.Collections.Generic.Dictionary<string, Nexus.Engine.Extensions.Models.ExtensionMetadata> extensions)
384+
{
385+
extensions[extensionName] = metadata;
386+
}
387+
388+
// Act
389+
var (isValid, errorMessage) = m_Manager.ValidateExtension(extensionName);
390+
391+
// Assert
392+
_ = isValid.Should().BeFalse();
393+
_ = errorMessage.Should().Contain("no script file");
394+
}
395+
396+
/// <summary>
397+
/// Verifies that ValidateExtension returns invalid when script type is unsupported.
398+
/// </summary>
399+
[Fact]
400+
public void ValidateExtension_WhenScriptTypeIsUnsupported_ReturnsInvalid()
401+
{
402+
// Arrange
403+
m_Manager = new Manager(m_FileSystemMock.Object, m_Settings.Object);
404+
var extensionName = "TestExtension";
405+
var metadata = new Nexus.Engine.Extensions.Models.ExtensionMetadata
406+
{
407+
Name = extensionName,
408+
ScriptFile = "script.sh",
409+
ScriptType = "bash",
410+
FullScriptPath = "C:\\extensions\\TestExtension\\script.sh",
411+
};
412+
413+
// Manually add extension to manager's internal dictionary via reflection
414+
var extensionsField = typeof(Manager).GetField("m_Extensions", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
415+
if (extensionsField?.GetValue(m_Manager) is System.Collections.Generic.Dictionary<string, Nexus.Engine.Extensions.Models.ExtensionMetadata> extensions)
416+
{
417+
extensions[extensionName] = metadata;
418+
}
419+
420+
// Setup file system to return true for file exists check
421+
_ = m_FileSystemMock.Setup(fs => fs.FileExists(It.IsAny<string>())).Returns(true);
422+
423+
// Act
424+
var (isValid, errorMessage) = m_Manager.ValidateExtension(extensionName);
425+
426+
// Assert
427+
_ = isValid.Should().BeFalse();
428+
_ = errorMessage.Should().Contain("unsupported script type");
429+
}
430+
431+
/// <summary>
432+
/// Verifies that ValidateExtension returns invalid when script type is null or empty.
433+
/// </summary>
434+
[Fact]
435+
public void ValidateExtension_WhenScriptTypeIsEmpty_ReturnsInvalid()
436+
{
437+
// Arrange
438+
m_Manager = new Manager(m_FileSystemMock.Object, m_Settings.Object);
439+
var extensionName = "TestExtension";
440+
var metadata = new Nexus.Engine.Extensions.Models.ExtensionMetadata
441+
{
442+
Name = extensionName,
443+
ScriptFile = "script.ps1",
444+
ScriptType = string.Empty,
445+
FullScriptPath = "C:\\extensions\\TestExtension\\script.ps1",
446+
};
447+
448+
// Manually add extension to manager's internal dictionary via reflection
449+
var extensionsField = typeof(Manager).GetField("m_Extensions", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
450+
if (extensionsField?.GetValue(m_Manager) is System.Collections.Generic.Dictionary<string, Nexus.Engine.Extensions.Models.ExtensionMetadata> extensions)
451+
{
452+
extensions[extensionName] = metadata;
453+
}
454+
455+
// Setup file system to return true for file exists check
456+
_ = m_FileSystemMock.Setup(fs => fs.FileExists(It.IsAny<string>())).Returns(true);
457+
458+
// Act
459+
var (isValid, errorMessage) = m_Manager.ValidateExtension(extensionName);
460+
461+
// Assert
462+
_ = isValid.Should().BeFalse();
463+
_ = errorMessage.Should().Contain("no script type");
464+
}
465+
466+
/// <summary>
467+
/// Verifies that constructor handles absolute path configuration correctly.
468+
/// </summary>
469+
[Fact]
470+
public void Constructor_WithAbsolutePathConfiguration_UsesPathAsIs()
471+
{
472+
// Arrange
473+
var absolutePath = "C:\\Absolute\\Extensions\\Path";
474+
var sharedConfig = new SharedConfiguration
475+
{
476+
McpNexus = new McpNexusSettings
477+
{
478+
Extensions = new ExtensionsSettings
479+
{
480+
ExtensionsPath = absolutePath,
481+
CallbackPort = 0,
482+
},
483+
},
484+
};
485+
_ = m_Settings.Setup(s => s.Get()).Returns(sharedConfig);
486+
_ = m_FileSystemMock.Setup(fs => fs.DirectoryExists(It.Is<string>(p => p == absolutePath))).Returns(true);
487+
488+
// Act
489+
m_Manager = new Manager(m_FileSystemMock.Object, m_Settings.Object);
490+
491+
// Assert
492+
_ = m_Manager.Should().NotBeNull();
493+
m_FileSystemMock.Verify(fs => fs.DirectoryExists(It.Is<string>(p => p == absolutePath)), Times.AtLeastOnce);
494+
}
495+
496+
/// <summary>
497+
/// Verifies that LoadExtensionsAsync handles directory that doesn't exist.
498+
/// </summary>
499+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
500+
[Fact]
501+
public async Task LoadExtensionsAsync_WhenDirectoryDoesNotExist_CreatesDirectory()
502+
{
503+
// Arrange
504+
m_Manager = new Manager(m_FileSystemMock.Object, m_Settings.Object);
505+
_ = m_FileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(false);
506+
507+
// Act
508+
await m_Manager.LoadExtensionsAsync();
509+
510+
// Assert
511+
m_FileSystemMock.Verify(fs => fs.CreateDirectory(It.IsAny<string>()), Times.AtLeastOnce);
512+
}
513+
514+
/// <summary>
515+
/// Verifies that LoadExtensionsAsync propagates file system errors from GetFiles.
516+
/// </summary>
517+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
518+
[Fact]
519+
public async Task LoadExtensionsAsync_WhenGetFilesThrowsException_PropagatesException()
520+
{
521+
// Arrange
522+
m_Manager = new Manager(m_FileSystemMock.Object, m_Settings.Object);
523+
524+
// Reset mock after constructor setup
525+
m_FileSystemMock.Reset();
526+
_ = m_FileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(true);
527+
_ = m_FileSystemMock.Setup(fs => fs.GetFiles(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<SearchOption>()))
528+
.Throws(new UnauthorizedAccessException("Access denied"));
529+
530+
// Act & Assert - exception should propagate since GetFiles is not wrapped in try-catch
531+
_ = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () => await m_Manager.LoadExtensionsAsync());
532+
}
309533
}

unittests/nexus_protocol_unittests/Middleware/ResponseFormattingMiddlewareTests.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,85 @@ public async Task InvokeAsync_WithFileNotFoundException_Returns500()
149149
var responseBody = await reader.ReadToEndAsync();
150150
_ = responseBody.Should().Contain("File not found");
151151
}
152+
153+
/// <summary>
154+
/// Verifies that InvokeAsync handles OperationCanceledException gracefully without sending error response.
155+
/// </summary>
156+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
157+
[Fact]
158+
public async Task InvokeAsync_WithOperationCanceledException_DoesNotSendErrorResponse()
159+
{
160+
RequestDelegate next = (HttpContext context) => throw new OperationCanceledException();
161+
var middleware = new ResponseFormattingMiddleware(next);
162+
var context = new DefaultHttpContext();
163+
context.Response.Body = new MemoryStream();
164+
165+
await middleware.InvokeAsync(context);
166+
167+
// Should not have written to response body
168+
_ = context.Response.StatusCode.Should().Be(200); // Default status code
169+
context.Response.Body.Position = 0;
170+
var reader = new StreamReader(context.Response.Body);
171+
var responseBody = await reader.ReadToEndAsync();
172+
_ = responseBody.Should().BeEmpty();
173+
}
174+
175+
/// <summary>
176+
/// Verifies that InvokeAsync handles exception when writing to response fails.
177+
/// </summary>
178+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
179+
[Fact]
180+
public async Task InvokeAsync_WhenWritingResponseFails_HandlesGracefully()
181+
{
182+
RequestDelegate next = (HttpContext context) => throw new InvalidOperationException("Test error");
183+
var middleware = new ResponseFormattingMiddleware(next);
184+
var context = new DefaultHttpContext();
185+
var throwingStream = new ThrowingMemoryStream();
186+
context.Response.Body = throwingStream;
187+
188+
// Should not throw even if writing fails
189+
await middleware.InvokeAsync(context);
190+
}
191+
192+
/// <summary>
193+
/// Memory stream that throws exception on write.
194+
/// </summary>
195+
private class ThrowingMemoryStream : MemoryStream
196+
{
197+
/// <summary>
198+
/// Writes to the stream, throwing exception after first write.
199+
/// </summary>
200+
/// <param name="buffer">The buffer to write.</param>
201+
/// <param name="offset">The offset.</param>
202+
/// <param name="count">The count.</param>
203+
public override void Write(byte[] buffer, int offset, int count)
204+
{
205+
if (Position > 0)
206+
{
207+
throw new IOException("Write failed");
208+
}
209+
210+
base.Write(buffer, offset, count);
211+
}
212+
213+
/// <summary>
214+
/// Writes to the stream asynchronously, throwing exception after first write.
215+
/// </summary>
216+
/// <param name="buffer">The buffer to write.</param>
217+
/// <param name="offset">The offset.</param>
218+
/// <param name="count">The count.</param>
219+
/// <param name="cancellationToken">The cancellation token.</param>
220+
/// <returns>The write task.</returns>
221+
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
222+
{
223+
#pragma warning disable IDE0046 // Convert to conditional expression - keeping explicit throw for clarity
224+
if (Position > 0)
225+
{
226+
throw new IOException("Write failed");
227+
}
228+
#pragma warning restore IDE0046
229+
230+
return base.WriteAsync(buffer, offset, count, cancellationToken);
231+
}
232+
}
152233
}

0 commit comments

Comments
 (0)