Skip to content

Commit 2e067ef

Browse files
committed
Add package operation tests
1 parent 1def0c8 commit 2e067ef

File tree

2 files changed

+319
-1
lines changed

2 files changed

+319
-1
lines changed

src/NetContextServer/Services/FileValidationService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace NetContextServer.Services;
44
/// Provides validation and path manipulation services for file system operations.
55
/// Ensures file access is restricted to the designated base directory for security.
66
/// </summary>
7-
internal static class FileValidationService
7+
public static class FileValidationService
88
{
99
/// <summary>
1010
/// Gets or sets the base directory for all file operations.
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
using ModelContextProtocol.Client;
2+
using ModelContextProtocol.Protocol.Types;
3+
using NetContextServer.Models;
4+
using NetContextServer.Services;
5+
using System.Text.Json;
6+
7+
namespace NetContextServer.Tests;
8+
9+
[Collection("NetContextServer Collection")]
10+
public class PackageOperationTests : IAsyncLifetime
11+
{
12+
private readonly string _testProjectDir;
13+
private readonly string _testProjectPath;
14+
private readonly NetContextServerFixture _fixture;
15+
private readonly JsonSerializerOptions _jsonOptions;
16+
17+
public PackageOperationTests(NetContextServerFixture fixture)
18+
{
19+
_fixture = fixture;
20+
_testProjectDir = Path.Combine(Path.GetTempPath(), "PackageOperationTests");
21+
Directory.CreateDirectory(_testProjectDir);
22+
_testProjectPath = Path.Combine(_testProjectDir, "test.csproj");
23+
_jsonOptions = new JsonSerializerOptions
24+
{
25+
WriteIndented = true,
26+
PropertyNameCaseInsensitive = true,
27+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
28+
};
29+
}
30+
31+
public async Task InitializeAsync()
32+
{
33+
// Set up the test environment
34+
FileValidationService.SetBaseDirectory(_testProjectDir);
35+
36+
// Ensure server is ready by sending a hello request
37+
var result = await _fixture.Client.CallToolAsync("hello", new Dictionary<string, object?>());
38+
if (result.IsError)
39+
{
40+
throw new InvalidOperationException("Failed to initialize server connection");
41+
}
42+
43+
// Set the base directory in the server
44+
result = await _fixture.Client.CallToolAsync("set_base_directory",
45+
new Dictionary<string, object?> { ["directory"] = _testProjectDir });
46+
if (result.IsError)
47+
{
48+
throw new InvalidOperationException("Failed to set base directory");
49+
}
50+
}
51+
52+
public async Task DisposeAsync()
53+
{
54+
try
55+
{
56+
if (Directory.Exists(_testProjectDir))
57+
{
58+
await Task.Run(() => Directory.Delete(_testProjectDir, true));
59+
}
60+
}
61+
catch (Exception ex)
62+
{
63+
Console.WriteLine($"Error cleaning up test directory: {ex.Message}");
64+
}
65+
}
66+
67+
private async Task CreateTestProjectFileAsync(string content)
68+
{
69+
await File.WriteAllTextAsync(_testProjectPath, content);
70+
71+
// Give the file system a moment to catch up
72+
await Task.Delay(100);
73+
}
74+
75+
private T? DeserializeResponse<T>(CallToolResponse response) where T : class
76+
{
77+
var jsonText = response.Content.FirstOrDefault(c => c.Type == "text")?.Text;
78+
Assert.NotNull(jsonText);
79+
80+
// Log the actual JSON for debugging
81+
Console.WriteLine($"Raw JSON response: {jsonText}");
82+
83+
try
84+
{
85+
// First try to parse as JsonDocument to check the structure
86+
using var doc = JsonDocument.Parse(jsonText);
87+
88+
// If it's an array and we're expecting a list, deserialize directly
89+
if (doc.RootElement.ValueKind == JsonValueKind.Array &&
90+
typeof(T).IsGenericType &&
91+
typeof(T).GetGenericTypeDefinition() == typeof(List<>))
92+
{
93+
return JsonSerializer.Deserialize<T>(jsonText, _jsonOptions);
94+
}
95+
96+
// If it's an object, check for error/message properties
97+
if (doc.RootElement.ValueKind == JsonValueKind.Object)
98+
{
99+
// Check if it's an error response
100+
if (doc.RootElement.TryGetProperty("error", out var errorElement))
101+
{
102+
Console.WriteLine($"Error response: {errorElement}");
103+
return null;
104+
}
105+
106+
// Check if it's a message response
107+
if (doc.RootElement.TryGetProperty("message", out var messageElement))
108+
{
109+
if (messageElement.GetString()?.Contains("No project files found") == true)
110+
{
111+
// Return an empty list for "no projects" message
112+
if (typeof(T) == typeof(List<ProjectPackageAnalysis>))
113+
{
114+
return (T)(object)new List<ProjectPackageAnalysis>();
115+
}
116+
}
117+
Console.WriteLine($"Message response: {messageElement}");
118+
return null;
119+
}
120+
121+
return JsonSerializer.Deserialize<T>(jsonText, _jsonOptions);
122+
}
123+
124+
throw new InvalidOperationException($"Unexpected JSON root type: {doc.RootElement.ValueKind}");
125+
}
126+
catch (JsonException ex)
127+
{
128+
Console.WriteLine($"JSON deserialization error: {ex.Message}");
129+
throw;
130+
}
131+
}
132+
133+
[Fact]
134+
public async Task AnalyzePackagesAsync_WithValidProject_ReturnsAnalysis()
135+
{
136+
// Arrange
137+
var projectContent = @"
138+
<Project Sdk=""Microsoft.NET.Sdk"">
139+
<ItemGroup>
140+
<PackageReference Include=""xunit"" Version=""2.4.2"" />
141+
<PackageReference Include=""Moq"" Version=""4.18.4"" />
142+
</ItemGroup>
143+
</Project>";
144+
await CreateTestProjectFileAsync(projectContent);
145+
146+
// Act
147+
var result = await _fixture.Client.CallToolAsync("analyze_packages", new Dictionary<string, object?>());
148+
var analysis = DeserializeResponse<List<ProjectPackageAnalysis>>(result);
149+
150+
// Assert
151+
Assert.NotNull(analysis);
152+
Assert.Single(analysis); // One project
153+
Assert.Equal(2, analysis[0].Packages.Count); // Two packages
154+
Assert.Contains(analysis[0].Packages, p => p.PackageId == "xunit");
155+
Assert.Contains(analysis[0].Packages, p => p.PackageId == "Moq");
156+
}
157+
158+
[Fact]
159+
public async Task AnalyzePackagesAsync_WithEmptyProject_ReturnsEmptyAnalysis()
160+
{
161+
// Arrange
162+
var projectContent = @"
163+
<Project Sdk=""Microsoft.NET.Sdk"">
164+
<ItemGroup>
165+
</ItemGroup>
166+
</Project>";
167+
await CreateTestProjectFileAsync(projectContent);
168+
169+
// Act
170+
var result = await _fixture.Client.CallToolAsync("analyze_packages", new Dictionary<string, object?>());
171+
var analysis = DeserializeResponse<List<ProjectPackageAnalysis>>(result);
172+
173+
// Assert
174+
Assert.NotNull(analysis);
175+
Assert.Single(analysis);
176+
Assert.Empty(analysis[0].Packages);
177+
}
178+
179+
[Fact]
180+
public async Task AnalyzePackagesAsync_WithInvalidProject_HandlesError()
181+
{
182+
// Arrange
183+
var projectContent = "invalid xml content";
184+
await CreateTestProjectFileAsync(projectContent);
185+
186+
// Act
187+
var result = await _fixture.Client.CallToolAsync("analyze_packages", new Dictionary<string, object?>());
188+
var jsonText = result.Content.FirstOrDefault(c => c.Type == "text")?.Text;
189+
Assert.NotNull(jsonText);
190+
191+
using var doc = JsonDocument.Parse(jsonText);
192+
// Either an error or a "no projects found" message is acceptable
193+
Assert.True(
194+
doc.RootElement.TryGetProperty("error", out _) ||
195+
(doc.RootElement.TryGetProperty("message", out var msg) &&
196+
msg.GetString()?.Contains("No project files found") == true),
197+
$"Expected error or no projects message in response. Actual JSON: {jsonText}");
198+
}
199+
200+
[Fact]
201+
public async Task AnalyzePackagesAsync_WithPreviewVersions_IncludesPreviewUpdates()
202+
{
203+
// Arrange
204+
var projectContent = @"
205+
<Project Sdk=""Microsoft.NET.Sdk"">
206+
<ItemGroup>
207+
<PackageReference Include=""Microsoft.Extensions.Hosting"" Version=""8.0.0"" />
208+
</ItemGroup>
209+
</Project>";
210+
await CreateTestProjectFileAsync(projectContent);
211+
212+
// Act
213+
var result = await _fixture.Client.CallToolAsync("analyze_packages",
214+
new Dictionary<string, object?> { ["includePreviewVersions"] = true });
215+
var analysis = DeserializeResponse<List<ProjectPackageAnalysis>>(result);
216+
217+
// Assert
218+
Assert.NotNull(analysis);
219+
Assert.Single(analysis);
220+
var package = analysis[0].Packages.First();
221+
Assert.Equal("Microsoft.Extensions.Hosting", package.PackageId);
222+
223+
// Either should have an update or a preview update
224+
Assert.True(package.HasUpdate || package.HasPreviewUpdate);
225+
}
226+
227+
[Fact]
228+
public async Task AnalyzePackagesAsync_WithDependencyGraph_GeneratesGraph()
229+
{
230+
// Arrange
231+
var projectContent = @"
232+
<Project Sdk=""Microsoft.NET.Sdk"">
233+
<ItemGroup>
234+
<PackageReference Include=""Microsoft.Extensions.DependencyInjection"" Version=""8.0.0"" />
235+
</ItemGroup>
236+
</Project>";
237+
await CreateTestProjectFileAsync(projectContent);
238+
239+
// Act
240+
var result = await _fixture.Client.CallToolAsync("analyze_packages", new Dictionary<string, object?>());
241+
var analysis = DeserializeResponse<List<ProjectPackageAnalysis>>(result);
242+
243+
// Assert
244+
Assert.NotNull(analysis);
245+
Assert.Single(analysis);
246+
var package = analysis[0].Packages.First();
247+
248+
// Verify dependency graph
249+
Assert.NotNull(package.DependencyGraph);
250+
Assert.Contains("Microsoft.Extensions.DependencyInjection", package.DependencyGraph);
251+
Assert.Contains("└─", package.DependencyGraph); // Should contain tree structure characters
252+
}
253+
254+
[Fact]
255+
public async Task AnalyzePackagesAsync_WithTestProject_DetectsImplicitUsage()
256+
{
257+
// Arrange
258+
var projectContent = @"
259+
<Project Sdk=""Microsoft.NET.Sdk"">
260+
<PropertyGroup>
261+
<IsPackable>false</IsPackable>
262+
</PropertyGroup>
263+
<ItemGroup>
264+
<PackageReference Include=""xunit"" Version=""2.4.2"" />
265+
<PackageReference Include=""xunit.runner.visualstudio"" Version=""2.4.5"" />
266+
<PackageReference Include=""coverlet.collector"" Version=""6.0.0"" />
267+
</ItemGroup>
268+
</Project>";
269+
await CreateTestProjectFileAsync(projectContent);
270+
271+
// Create a test file with xUnit attributes
272+
var testFilePath = Path.Combine(_testProjectDir, "UnitTest1.cs");
273+
await File.WriteAllTextAsync(testFilePath, @"
274+
using Xunit;
275+
namespace TestProject;
276+
public class UnitTest1
277+
{
278+
[Fact]
279+
public void Test1()
280+
{
281+
Assert.True(true);
282+
}
283+
}");
284+
285+
// Act
286+
var result = await _fixture.Client.CallToolAsync("analyze_packages", new Dictionary<string, object?>());
287+
var analysis = DeserializeResponse<List<ProjectPackageAnalysis>>(result);
288+
289+
// Assert
290+
Assert.NotNull(analysis);
291+
Assert.Single(analysis);
292+
293+
// Verify test packages are marked as implicitly used
294+
foreach (var package in analysis[0].Packages)
295+
{
296+
Assert.True(package.IsUsed);
297+
Assert.True(package.ImplicitUsage);
298+
Assert.NotEmpty(package.UsageLocations);
299+
}
300+
}
301+
302+
[Fact]
303+
public async Task AnalyzePackagesAsync_WithNoProjects_ReturnsMessage()
304+
{
305+
// Arrange - Empty directory
306+
Directory.Delete(_testProjectDir, true);
307+
Directory.CreateDirectory(_testProjectDir);
308+
309+
// Act
310+
var result = await _fixture.Client.CallToolAsync("analyze_packages", new Dictionary<string, object?>());
311+
var jsonText = result.Content.FirstOrDefault(c => c.Type == "text")?.Text;
312+
Assert.NotNull(jsonText);
313+
314+
using var doc = JsonDocument.Parse(jsonText);
315+
Assert.True(doc.RootElement.TryGetProperty("message", out var messageValue));
316+
Assert.Contains("No project files found", messageValue.GetString());
317+
}
318+
}

0 commit comments

Comments
 (0)