Skip to content

Commit 44d423a

Browse files
committed
Add unit tests for API endpoints, AI configuration, and chat service functionality
- Implement ApiEndpointSimpleTests for CRUD operations on conversations. - Create AIConfigurationTests to validate AI provider and model retrieval. - Add ChatServiceSimpleTests to ensure proper initialization and message processing. - Introduce MockChatClient for testing chat client interactions. - Enhance model validation tests for conversation models. - Establish integration tests for conversation and message models. - Update project files to include necessary dependencies and configurations.
1 parent 7e2272e commit 44d423a

41 files changed

Lines changed: 1822 additions & 160 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.editorconfig

Lines changed: 476 additions & 6 deletions
Large diffs are not rendered by default.

BuildWithAspire.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
</Folder>
1111
<Folder Name="/tests/">
1212
<Project Path="tests/BuildWithAspire.ApiService.UnitTests/BuildWithAspire.ApiService.UnitTests.csproj" />
13+
<Project Path="tests/BuildWithAspire.AppHost.UnitTests/BuildWithAspire.AppHost.UnitTests.csproj" />
1314
<Project Path="tests/BuildWithAspire.Web.UnitTests/BuildWithAspire.Web.UnitTests.csproj" />
1415
</Folder>
1516
</Solution>

src/BuildWithAspire.ApiService/BuildWithAspire.ApiService.csproj

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<ItemGroup>
1111
<PackageReference Include="Aspire.Azure.AI.OpenAI" Version="9.3.1-preview.1.25305.6" />
1212
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.3.1" />
13-
<PackageReference Include="CommunityToolkit.Aspire.OllamaSharp" Version="9.5.1-beta.318" />
13+
<PackageReference Include="CommunityToolkit.Aspire.OllamaSharp" Version="9.5.1-beta.319" />
1414
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
1515
<PackageReference Include="Azure.Identity" Version="1.14.1" />
1616
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
@@ -23,8 +23,7 @@
2323
<PackageReference Include="Microsoft.Extensions.AI" Version="9.6.0" />
2424
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.6.0-preview.1.25310.2" />
2525
<PackageReference Include="Microsoft.AI.Foundry.Local" Version="0.1.0" />
26-
<PackageReference Include="OpenAI" Version="2.2.0-beta.4" />
27-
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" />
26+
<PackageReference Include="Scalar.AspNetCore" Version="2.5.6" />
2827
<PackageReference Include="System.Text.Json" Version="9.0.6" />
2928
<PackageReference Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.59.0" />
3029
</ItemGroup>

src/BuildWithAspire.ApiService/Configuration/AIConfiguration.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,4 @@ public static string GetModel(IConfiguration configuration, AIProvider? provider
6060
_ => throw new InvalidOperationException($"No default model available for provider: {provider}")
6161
};
6262
}
63-
}
63+
}

src/BuildWithAspire.ApiService/Data/ChatDbContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
4141
entity.HasIndex(e => e.ConversationId);
4242
});
4343
}
44-
}
44+
}

src/BuildWithAspire.ApiService/Extensions/AIServiceExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using BuildWithAspire.ApiService.Configuration;
2-
using Microsoft.Extensions.AI;
32
using Microsoft.AI.Foundry.Local;
3+
using Microsoft.Extensions.AI;
44
using OpenAI;
55

66
namespace BuildWithAspire.ApiService.Extensions;
@@ -108,7 +108,7 @@ private static void AddFoundryLocalAIServices(this IHostApplicationBuilder build
108108
/// <summary>
109109
/// Background service that logs AI configuration on startup.
110110
/// </summary>
111-
internal class AIConfigurationLogger : BackgroundService
111+
internal sealed class AIConfigurationLogger : BackgroundService
112112
{
113113
private readonly ILogger<AIConfigurationLogger> _logger;
114114
private readonly AIConfiguration.AISettings _aiSettings;
@@ -125,7 +125,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
125125
_aiSettings.Provider, _aiSettings.Model, _aiSettings.DeploymentName);
126126

127127
// Complete immediately - this is just for logging
128-
await Task.CompletedTask;
128+
await Task.CompletedTask.ConfigureAwait(false);
129129
}
130130
}
131131

src/BuildWithAspire.ApiService/Models/ChatMessageRequest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ public class ChatMessageRequest
44
{
55
public string Role { get; set; } = string.Empty;
66
public string Content { get; set; } = string.Empty;
7-
}
7+
}

src/BuildWithAspire.ApiService/Models/Conversation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ public class Conversation
99

1010
// Navigation property
1111
public List<Message> Messages { get; set; } = new();
12-
}
12+
}

src/BuildWithAspire.ApiService/Models/Message.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ public class Message
1313
// Navigation property
1414
[JsonIgnore]
1515
public Conversation Conversation { get; set; } = null!;
16-
}
16+
}

src/BuildWithAspire.ApiService/Program.cs

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.AspNetCore.Mvc;
66
using Microsoft.EntityFrameworkCore;
77
using Microsoft.Extensions.AI;
8+
using Scalar.AspNetCore;
89
using ChatRole = Microsoft.Extensions.AI.ChatRole;
910

1011
var builder = WebApplication.CreateBuilder(args);
@@ -19,9 +20,18 @@
1920

2021
builder.Services.AddKernel();
2122

22-
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
23+
// Learn more about configuring OpenAPI at https://aka.ms/aspnetcore/openapi
2324
builder.Services.AddEndpointsApiExplorer();
24-
builder.Services.AddSwaggerGen();
25+
builder.Services.AddOpenApi(options =>
26+
{
27+
options.AddDocumentTransformer((document, context, cancellationToken) =>
28+
{
29+
document.Info.Title = "BuildWithAspire API";
30+
document.Info.Description = "API for BuildWithAspire application with AI chat capabilities";
31+
document.Info.Version = "v1";
32+
return Task.CompletedTask;
33+
});
34+
});
2535

2636
builder.Services.AddTransient<ChatService>();
2737

@@ -32,8 +42,12 @@
3242
// Configure the HTTP request pipeline.
3343
if (app.Environment.IsDevelopment())
3444
{
35-
app.UseSwagger();
36-
app.UseSwaggerUI();
45+
app.MapOpenApi();
46+
app.MapScalarApiReference(options =>
47+
{
48+
options.WithTitle("BuildWithAspire API");
49+
options.WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient);
50+
});
3751
}
3852

3953
app.UseHttpsRedirection();
@@ -48,7 +62,7 @@
4862
var startTime = DateTime.UtcNow;
4963

5064
// Ensure database schema exists
51-
var wasCreated = await dbContext.Database.EnsureCreatedAsync();
65+
var wasCreated = await dbContext.Database.EnsureCreatedAsync().ConfigureAwait(false);
5266
var duration = DateTime.UtcNow - startTime;
5367

5468
if (wasCreated)
@@ -66,15 +80,14 @@
6680
app.Logger.LogError(ex, "Database initialization failed. Service will continue without database.");
6781
}
6882

69-
7083
app.MapGet("/weatherforecast", (IChatClient client) =>
7184
{
7285
async IAsyncEnumerable<WeatherForecast> GetForecasts()
7386
{
7487
for (int index = 1; index <= 5; index++)
7588
{
7689
var temperature = Random.Shared.Next(-20, 55);
77-
var summary = await GetWeatherSummary(client, temperature);
90+
var summary = await GetWeatherSummary(client, temperature).ConfigureAwait(false);
7891
yield return new WeatherForecast
7992
(
8093
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
@@ -95,7 +108,7 @@ static async Task<string> GetWeatherSummary(IChatClient client, int temp)
95108
// User messages represent user input, whether historical or the most recent input
96109
new(ChatRole.User, $"How would you describe the weather at temp {temp} in celcius? Provide the response in 1 word with no punctuation.")
97110
};
98-
var completion = await client.GetResponseAsync(conversation);
111+
var completion = await client.GetResponseAsync(conversation).ConfigureAwait(false);
99112

100113
return $"{completion.Text}";
101114
}
@@ -118,7 +131,7 @@ static async Task<string> GetWeatherSummary(IChatClient client, int temp)
118131
c.UpdatedAt,
119132
MessageCount = c.Messages.Count()
120133
})
121-
.ToListAsync();
134+
.ToListAsync().ConfigureAwait(false);
122135

123136
return Results.Ok(conversations);
124137
}
@@ -137,7 +150,7 @@ static async Task<string> GetWeatherSummary(IChatClient client, int temp)
137150
{
138151
var conversation = await db.Conversations
139152
.Include(c => c.Messages.OrderBy(m => m.CreatedAt))
140-
.FirstOrDefaultAsync(c => c.Id == id);
153+
.FirstOrDefaultAsync(c => c.Id == id).ConfigureAwait(false);
141154

142155
if (conversation == null)
143156
{
@@ -160,7 +173,9 @@ static async Task<string> GetWeatherSummary(IChatClient client, int temp)
160173
try
161174
{
162175
if (string.IsNullOrWhiteSpace(request.Name))
176+
{
163177
return Results.BadRequest("Conversation name cannot be empty");
178+
}
164179

165180
var conversation = new Conversation
166181
{
@@ -171,7 +186,7 @@ static async Task<string> GetWeatherSummary(IChatClient client, int temp)
171186
};
172187

173188
db.Conversations.Add(conversation);
174-
await db.SaveChangesAsync();
189+
await db.SaveChangesAsync().ConfigureAwait(false);
175190

176191
return Results.Created($"/conversations/{conversation.Id}", conversation);
177192
}
@@ -190,14 +205,18 @@ static async Task<string> GetWeatherSummary(IChatClient client, int temp)
190205
{
191206
// Validate input
192207
if (string.IsNullOrWhiteSpace(request.Message))
208+
{
193209
return Results.BadRequest("Message cannot be empty");
210+
}
194211

195212
var conversation = await db.Conversations
196213
.Include(c => c.Messages)
197-
.FirstOrDefaultAsync(c => c.Id == id);
214+
.FirstOrDefaultAsync(c => c.Id == id).ConfigureAwait(false);
198215

199216
if (conversation == null)
217+
{
200218
return Results.NotFound("Conversation not found");
219+
}
201220

202221
// Add user message
203222
var userMessage = new Message
@@ -214,7 +233,7 @@ static async Task<string> GetWeatherSummary(IChatClient client, int temp)
214233
conversation.UpdatedAt = DateTime.UtcNow;
215234

216235
// Save user message first
217-
await db.SaveChangesAsync();
236+
await db.SaveChangesAsync().ConfigureAwait(false);
218237

219238
// Prepare history for AI - get all messages for this conversation including the new one
220239
var messages = await db.Messages
@@ -225,13 +244,13 @@ static async Task<string> GetWeatherSummary(IChatClient client, int temp)
225244
Role = m.Role,
226245
Content = m.Content
227246
})
228-
.ToListAsync();
247+
.ToListAsync().ConfigureAwait(false);
229248

230249
// Get AI response with timeout handling
231250
string aiResponse;
232251
try
233252
{
234-
aiResponse = await chatService.ProcessMessagesWithHistory(messages);
253+
aiResponse = await chatService.ProcessMessagesWithHistory(messages).ConfigureAwait(false);
235254
}
236255
catch (Exception ex)
237256
{
@@ -251,7 +270,7 @@ static async Task<string> GetWeatherSummary(IChatClient client, int temp)
251270

252271
// Add to DbContext directly instead of through navigation property
253272
db.Messages.Add(assistantMessage);
254-
await db.SaveChangesAsync();
273+
await db.SaveChangesAsync().ConfigureAwait(false);
255274

256275
return Results.Ok(new { response = aiResponse });
257276
}
@@ -266,30 +285,35 @@ static async Task<string> GetWeatherSummary(IChatClient client, int temp)
266285

267286
app.MapDelete("/conversations/{id}", async (Guid id, ChatDbContext db) =>
268287
{
269-
var conversation = await db.Conversations.FindAsync(id);
288+
var conversation = await db.Conversations.FindAsync(id).ConfigureAwait(false);
270289
if (conversation == null)
290+
{
271291
return Results.NotFound();
292+
}
272293

273294
db.Conversations.Remove(conversation);
274-
await db.SaveChangesAsync();
295+
await db.SaveChangesAsync().ConfigureAwait(false);
275296

276297
return Results.NoContent();
277298
})
278299
.WithName("DeleteConversation")
279300
.WithOpenApi();
280301

281302
// Keep the original chat endpoint for backward compatibility
282-
app.MapGet("/chat", async (ChatService chatService, string message) => await chatService.ProcessMessage(message))
303+
app.MapGet("/chat", async (ChatService chatService, string message) => await chatService.ProcessMessage(message).ConfigureAwait(false))
283304
.WithName("GetChat")
284305
.WithOpenApi();
285306

286307
app.Run();
287308

288-
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
309+
public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
289310
{
290311
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
291312
}
292313

293314
// Request DTOs
294315
public record CreateConversationRequest(string Name);
295316
public record SendMessageRequest(string Message);
317+
318+
// Make Program class accessible for testing
319+
public partial class Program { }

0 commit comments

Comments
 (0)