Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/PR-Build-And-Test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
run: dotnet build --configuration Release --no-restore /p:AccessToNugetFeed=false

- name: Run .NET Tests
run: dotnet test --no-build --configuration Release --logger trx --results-directory ${{ runner.temp }}
run: dotnet test --no-build --configuration Release --report-trx --results-directory ${{ runner.temp }}

- name: Convert TRX to VS Playlist
if: failure()
Expand Down
5 changes: 1 addition & 4 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.4.0" />
<PackageVersion Include="Microsoft.ApplicationInsights.Profiler.AspNetCore" Version="3.0.1" />
<PackageVersion Include="coverlet.collector" Version="8.0.0" />
<PackageVersion Include="TUnit" Version="1.17.11" />
<PackageVersion Include="EssentialCSharp.Shared.Models" Version="$(ToolingPackagesVersion)" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
<PackageVersion Include="IntelliTect.Multitool" Version="1.5.3" />
Expand All @@ -36,7 +36,6 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.72.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.PgVector" Version="1.70.0-preview" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.103" />
Expand All @@ -50,7 +49,5 @@
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="Octokit" Version="14.0.0" />
<PackageVersion Include="DotnetSitemapGenerator" Version="2.0.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
</Project>
8 changes: 1 addition & 7 deletions EssentialCSharp.Chat.Tests/EssentialCSharp.Chat.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="TUnit" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\EssentialCSharp.Chat\EssentialCSharp.Chat.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
82 changes: 52 additions & 30 deletions EssentialCSharp.Chat.Tests/MarkdownChunkingServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ namespace EssentialCSharp.Chat.Tests;
public class MarkdownChunkingServiceTests
{
#region MarkdownContentToHeadersAndSection
[Fact]
public void MarkdownContentToHeadersAndSection_ParsesSampleMarkdown_CorrectlyCombinesHeadersAndExtractsContent()
[Test]
public async Task MarkdownContentToHeadersAndSection_ParsesSampleMarkdown_CorrectlyCombinesHeadersAndExtractsContent()
{
string markdown = """
### Beginner Topic
Expand Down Expand Up @@ -43,15 +43,23 @@ publicstaticvoid Main() // Method declaration

var sections = MarkdownChunkingService.MarkdownContentToHeadersAndSection(markdown);

Assert.Equal(3, sections.Count);
Assert.Contains(sections, s => s.Header == "Beginner Topic: What Is a Method?" && string.Join("\n", s.Content).Contains("Syntactically, a **method** in C# is a named block of code"));
Assert.Contains(sections, s => s.Header == "Main Method" && string.Join("\n", s.Content).Contains("The location where C# programs begin execution is the **Main method**, which begins with `static void Main()`")
&& string.Join("\n", s.Content).Contains("publicclass Program"));
Assert.Contains(sections, s => s.Header == "Main Method: Advanced Topic: Declaration of the Main Method" && string.Join("\n", s.Content).Contains("C# requires that the Main method return either `void` or `int`"));
await Assert.That(sections.Count).IsEqualTo(3);
var beginnerSection = sections.FirstOrDefault(s => s.Header == "Beginner Topic: What Is a Method?");
await Assert.That(beginnerSection.Header).IsNotNull();
await Assert.That(string.Join("\n", beginnerSection.Content)).Contains("Syntactically, a **method** in C# is a named block of code");

var mainMethodSection = sections.FirstOrDefault(s => s.Header == "Main Method");
await Assert.That(mainMethodSection.Header).IsNotNull();
await Assert.That(string.Join("\n", mainMethodSection.Content)).Contains("The location where C# programs begin execution is the **Main method**, which begins with `static void Main()`");
await Assert.That(string.Join("\n", mainMethodSection.Content)).Contains("publicclass Program");

var advancedTopicSection = sections.FirstOrDefault(s => s.Header == "Main Method: Advanced Topic: Declaration of the Main Method");
await Assert.That(advancedTopicSection.Header).IsNotNull();
await Assert.That(string.Join("\n", advancedTopicSection.Content)).Contains("C# requires that the Main method return either `void` or `int`");
}

[Fact]
public void MarkdownContentToHeadersAndSection_AppendsCodeListingToPriorSection()
[Test]
public async Task MarkdownContentToHeadersAndSection_AppendsCodeListingToPriorSection()
{
string markdown = """
## Working with Variables
Expand Down Expand Up @@ -86,16 +94,16 @@ publicstaticvoid Main()

var sections = MarkdownChunkingService.MarkdownContentToHeadersAndSection(markdown);

Assert.Equal(2, sections.Count);
await Assert.That(sections.Count).IsEqualTo(2);
// The code listing should be appended to the Working with Variables section, not as its own section
var workingWithVariablesSection = sections.FirstOrDefault(s => s.Header == "Working with Variables");
Assert.True(!string.IsNullOrEmpty(workingWithVariablesSection.Header));
Assert.Contains("publicclass MiracleMax", string.Join("\n", workingWithVariablesSection.Content));
Assert.DoesNotContain(sections, s => s.Header == "Listing 1.12: Declaring and Assigning a Variable");
await Assert.That(workingWithVariablesSection.Header).IsNotNull().And.IsNotEmpty();
await Assert.That(string.Join("\n", workingWithVariablesSection.Content)).Contains("publicclass MiracleMax");
await Assert.That(sections).DoesNotContain(s => s.Header == "Listing 1.12: Declaring and Assigning a Variable");
}

[Fact]
public void MarkdownContentToHeadersAndSection_KeepsPriorHeadersAppended()
[Test]
public async Task MarkdownContentToHeadersAndSection_KeepsPriorHeadersAppended()
{
string markdown = """
### Beginner Topic
Expand Down Expand Up @@ -143,19 +151,33 @@ publicstaticvoid Main()
""";

var sections = MarkdownChunkingService.MarkdownContentToHeadersAndSection(markdown);
Assert.Equal(5, sections.Count);
await Assert.That(sections.Count).IsEqualTo(5);

var beginnerDataTypeSection = sections.FirstOrDefault(s => s.Header == "Beginner Topic: What Is a Data Type?");
await Assert.That(beginnerDataTypeSection.Header).IsNotNull();
await Assert.That(string.Join("\n", beginnerDataTypeSection.Content)).Contains("The type of data that a variable declaration specifies is called a **data type**");

var declaringSection = sections.FirstOrDefault(s => s.Header == "Declaring a Variable");
await Assert.That(declaringSection.Header).IsNotNull();
await Assert.That(string.Join("\n", declaringSection.Content)).Contains("In Listing 1.12, `string max` is a variable declaration");

var declaringAnotherSection = sections.FirstOrDefault(s => s.Header == "Declaring a Variable: Declaring another thing");
await Assert.That(declaringAnotherSection.Header).IsNotNull();
await Assert.That(string.Join("\n", declaringAnotherSection.Content)).Contains("Because a multivariable declaration statement allows developers to provide the data type only once");

var assigningSection = sections.FirstOrDefault(s => s.Header == "Assigning a Variable");
await Assert.That(assigningSection.Header).IsNotNull();
await Assert.That(string.Join("\n", assigningSection.Content)).Contains("After declaring a local variable, you must assign it a value before reading from it.");

Assert.Contains(sections, s => s.Header == "Beginner Topic: What Is a Data Type?" && string.Join("\n", s.Content).Contains("The type of data that a variable declaration specifies is called a **data type**"));
Assert.Contains(sections, s => s.Header == "Declaring a Variable" && string.Join("\n", s.Content).Contains("In Listing 1.12, `string max` is a variable declaration"));
Assert.Contains(sections, s => s.Header == "Declaring a Variable: Declaring another thing" && string.Join("\n", s.Content).Contains("Because a multivariable declaration statement allows developers to provide the data type only once"));
Assert.Contains(sections, s => s.Header == "Assigning a Variable" && string.Join("\n", s.Content).Contains("After declaring a local variable, you must assign it a value before reading from it."));
Assert.Contains(sections, s => s.Header == "Assigning a Variable: Continued Learning" && string.Join("\n", s.Content).Contains("From this listing, observe that it is possible to assign a variable as part of the variable declaration"));
var continuedLearningSection = sections.FirstOrDefault(s => s.Header == "Assigning a Variable: Continued Learning");
await Assert.That(continuedLearningSection.Header).IsNotNull();
await Assert.That(string.Join("\n", continuedLearningSection.Content)).Contains("From this listing, observe that it is possible to assign a variable as part of the variable declaration");
}
#endregion MarkdownContentToHeadersAndSection

#region ProcessSingleMarkdownFile
[Fact]
public void ProcessSingleMarkdownFile_ProducesExpectedChunksAndHeaders()
[Test]
public async Task ProcessSingleMarkdownFile_ProducesExpectedChunksAndHeaders()
{
// Arrange
var logger = new Mock<Microsoft.Extensions.Logging.ILogger<MarkdownChunkingService>>().Object;
Expand All @@ -178,13 +200,13 @@ public void ProcessSingleMarkdownFile_ProducesExpectedChunksAndHeaders()
var result = service.ProcessSingleMarkdownFile(fileContent, fileName, filePath);

// Assert
Assert.NotNull(result);
Assert.Equal(fileName, result.FileName);
Assert.Equal(filePath, result.FilePath);
Assert.Contains("This is the first section.", string.Join("\n", result.Chunks));
Assert.Contains("Console.WriteLine(\"Hello World\");", string.Join("\n", result.Chunks));
Assert.Contains("This is the second section.", string.Join("\n", result.Chunks));
Assert.Contains(result.Chunks, c => c.Contains("This is the second section."));
await Assert.That(result).IsNotNull();
await Assert.That(result.FileName).IsEqualTo(fileName);
await Assert.That(result.FilePath).IsEqualTo(filePath);
await Assert.That(string.Join("\n", result.Chunks)).Contains("This is the first section.");
await Assert.That(string.Join("\n", result.Chunks)).Contains("Console.WriteLine(\"Hello World\");");
await Assert.That(string.Join("\n", result.Chunks)).Contains("This is the second section.");
await Assert.That(result.Chunks).Contains(c => c.Contains("This is the second section."));
}
#endregion ProcessSingleMarkdownFile
}
Expand Down
11 changes: 1 addition & 10 deletions EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq.AutoMock" />
<PackageReference Include="TUnit" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
55 changes: 25 additions & 30 deletions EssentialCSharp.Web.Tests/FunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,52 @@

namespace EssentialCSharp.Web.Tests;

public class FunctionalTests
[ClassDataSource<WebApplicationFactory>(Shared = SharedType.PerClass)]
public class FunctionalTests(WebApplicationFactory factory)
{
[Theory]
[InlineData("/")]
[InlineData("/hello-world")]
[InlineData("/hello-world#hello-world")]
[InlineData("/guidelines")]
[InlineData("/healthz")]
[Test]
[Arguments("/")]
[Arguments("/hello-world")]
[Arguments("/hello-world#hello-world")]
[Arguments("/guidelines")]
[Arguments("/healthz")]
public async Task WhenTheApplicationStarts_ItCanLoadLoadPages(string relativeUrl)
{
using WebApplicationFactory factory = new();

HttpClient client = factory.CreateClient();
using HttpResponseMessage response = await client.GetAsync(relativeUrl);

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
}

[Theory]
[InlineData("/guidelines?rid=test-referral-id")]
[InlineData("/about?rid=abc123")]
[InlineData("/hello-world?rid=user-referral")]
[InlineData("/guidelines?rid=")]
[InlineData("/about?rid= ")]
[InlineData("/guidelines?foo=bar")]
[InlineData("/about?someOtherParam=value")]
[Test]
[Arguments("/guidelines?rid=test-referral-id")]
[Arguments("/about?rid=abc123")]
[Arguments("/hello-world?rid=user-referral")]
[Arguments("/guidelines?rid=")]
[Arguments("/about?rid= ")]
[Arguments("/guidelines?foo=bar")]
[Arguments("/about?someOtherParam=value")]
public async Task WhenPagesAreAccessed_TheyReturnHtml(string relativeUrl)
{
using WebApplicationFactory factory = new();

HttpClient client = factory.CreateClient();
using HttpResponseMessage response = await client.GetAsync(relativeUrl);

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);

// Ensure the response has content (not blank)
string content = await response.Content.ReadAsStringAsync();
Assert.NotEmpty(content);
await Assert.That(content).IsNotEmpty();

// Verify it's actually HTML content, not just whitespace
Assert.Contains("<html", content, StringComparison.OrdinalIgnoreCase);
await Assert.That(content).Contains("<html", StringComparison.OrdinalIgnoreCase);
}

[Fact]
[Test]
public async Task WhenTheApplicationStarts_NonExistingPage_GivesCorrectStatusCode()
{
using WebApplicationFactory factory = new();

HttpClient client = factory.CreateClient();
using HttpResponseMessage response = await client.GetAsync("/non-existing-page1234");

Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound);
}
}
}
86 changes: 46 additions & 40 deletions EssentialCSharp.Web.Tests/Integration/CaptchaTests.cs
Original file line number Diff line number Diff line change
@@ -1,42 +1,48 @@
using EssentialCSharp.Web.Models;
using EssentialCSharp.Web.Services;
using Microsoft.Extensions.Configuration;
using EssentialCSharp.Web.Models;
using EssentialCSharp.Web.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace EssentialCSharp.Web.Extensions.Tests.Integration;

public class CaptchaTests(CaptchaServiceProvider serviceProvider) : IClassFixture<CaptchaServiceProvider>
{
[Fact]
public async Task CaptchaService_Verify_Success()
{
ICaptchaService captchaService = serviceProvider.ServiceProvider.GetRequiredService<ICaptchaService>();

// From https://docs.hcaptcha.com/#integration-testing-test-keys
string hCaptchaSecret = "0x0000000000000000000000000000000000000000";
string hCaptchaToken = "10000000-aaaa-bbbb-cccc-000000000001";
string hCaptchaSiteKey = "10000000-ffff-ffff-ffff-000000000001";
HCaptchaResult? response = await captchaService.VerifyAsync(hCaptchaSecret, hCaptchaToken, hCaptchaSiteKey);

Assert.NotNull(response);
Assert.True(response.Success);
}
}

public class CaptchaServiceProvider
{
public ServiceProvider ServiceProvider { get; } = CreateServiceProvider();
public static ServiceProvider CreateServiceProvider()
{
IServiceCollection services = new ServiceCollection();

IConfigurationRoot configuration = new ConfigurationBuilder()
.SetBasePath(IntelliTect.Multitool.RepositoryPaths.GetDefaultRepoRoot())
.AddJsonFile($"{nameof(EssentialCSharp)}.{nameof(Web)}/appsettings.json")
.Build();
services.AddCaptchaService(configuration.GetSection(CaptchaOptions.CaptchaSender));
// Add other necessary services here

return services.BuildServiceProvider();
}
}
namespace EssentialCSharp.Web.Extensions.Tests.Integration;

[ClassDataSource<CaptchaServiceProvider>(Shared = SharedType.PerClass)]
public class CaptchaTests(CaptchaServiceProvider serviceProvider)
{
[Test]
public async Task CaptchaService_Verify_Success()
{
ICaptchaService captchaService = serviceProvider.ServiceProvider.GetRequiredService<ICaptchaService>();

// From https://docs.hcaptcha.com/#integration-testing-test-keys
string hCaptchaSecret = "0x0000000000000000000000000000000000000000";
string hCaptchaToken = "10000000-aaaa-bbbb-cccc-000000000001";
string hCaptchaSiteKey = "10000000-ffff-ffff-ffff-000000000001";
HCaptchaResult? response = await captchaService.VerifyAsync(hCaptchaSecret, hCaptchaToken, hCaptchaSiteKey);

await Assert.That(response).IsNotNull();
await Assert.That(response.Success).IsTrue();
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

response is HCaptchaResult? and is dereferenced (response.Success) after IsNotNull(). If TUnit’s IsNotNull() doesn’t inform nullable flow analysis, this will produce CS8602 and fail the build with warnings-as-errors. Use response! (or capture a non-null local) before accessing .Success.

Suggested change
await Assert.That(response.Success).IsTrue();
HCaptchaResult nonNullResponse = response!;
await Assert.That(nonNullResponse.Success).IsTrue();

Copilot uses AI. Check for mistakes.
}
}

public class CaptchaServiceProvider : IDisposable
{
public ServiceProvider ServiceProvider { get; } = CreateServiceProvider();
public static ServiceProvider CreateServiceProvider()
{
IServiceCollection services = new ServiceCollection();

IConfigurationRoot configuration = new ConfigurationBuilder()
.SetBasePath(IntelliTect.Multitool.RepositoryPaths.GetDefaultRepoRoot())
.AddJsonFile($"{nameof(EssentialCSharp)}.{nameof(Web)}/appsettings.json")
.Build();
services.AddCaptchaService(configuration.GetSection(CaptchaOptions.CaptchaSender));
// Add other necessary services here

return services.BuildServiceProvider();
}
public void Dispose()
{
ServiceProvider.Dispose();
GC.SuppressFinalize(this);
}
}
Loading
Loading