Skip to content
Merged
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
5 changes: 4 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,7 @@ csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent

# CA1848: Use the LoggerMessage delegates
dotnet_diagnostic.CA1848.severity = suggestion
dotnet_diagnostic.CA1848.severity = suggestion
# Test files - allow underscore-separated test method names (CA1707)
[{EssentialCSharp.Web.Tests,EssentialCSharp.Chat.Tests}/**]
dotnet_diagnostic.CA1707.severity = none
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 --coverage --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.25" />
<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>
9 changes: 2 additions & 7 deletions EssentialCSharp.Chat.Tests/EssentialCSharp.Chat.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,17 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsPublishable>false</IsPublishable>
</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>
71 changes: 35 additions & 36 deletions EssentialCSharp.Chat.Tests/MarkdownChunkingServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
using EssentialCSharp.Chat.Common.Services;
using EssentialCSharp.Chat.Common.Services;
using Moq;

namespace EssentialCSharp.Chat.Tests;
// TODO: Move to editorconfig later, just moving quick
#pragma warning disable CA1707 // Identifiers should not contain underscores

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 +42,16 @@ 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);
await Assert.That(sections).Contains(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"));

await Assert.That(sections).Contains(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"));

await Assert.That(sections).Contains(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`"));
}

[Fact]
public void MarkdownContentToHeadersAndSection_AppendsCodeListingToPriorSection()
[Test]
public async Task MarkdownContentToHeadersAndSection_AppendsCodeListingToPriorSection()
{
string markdown = """
## Working with Variables
Expand Down Expand Up @@ -86,16 +86,14 @@ 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(sections).Contains(s => s.Header == "Working with Variables" && string.Join("\n", s.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 +141,23 @@ publicstaticvoid Main()
""";

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

await Assert.That(sections).Contains(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 == "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"));
await Assert.That(sections).Contains(s => s.Header == "Declaring a Variable" && string.Join("\n", s.Content).Contains("In Listing 1.12, `string max` is a variable declaration"));

await Assert.That(sections).Contains(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"));

await Assert.That(sections).Contains(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."));

await Assert.That(sections).Contains(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"));
}
#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,15 +180,12 @@ 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(result.Chunks).Contains(c => c.Contains("This is the second section."));
}
#endregion ProcessSingleMarkdownFile
}

#pragma warning restore CA1707 // Identifiers should not contain underscores
15 changes: 1 addition & 14 deletions EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,14 @@

<IsPackable>false</IsPackable>
<IsPublishable>false</IsPublishable>
<!--
CA1707, Identifiers should not contain underscores - we allow these in test names
-->
<NoWarn>$(NoWarn);CA1707</NoWarn>
</PropertyGroup>

<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
56 changes: 26 additions & 30 deletions EssentialCSharp.Web.Tests/FunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,53 @@

namespace EssentialCSharp.Web.Tests;

public class FunctionalTests
[NotInParallel("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);
}
}
}
40 changes: 31 additions & 9 deletions EssentialCSharp.Web.Tests/Integration/CaptchaTests.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
using EssentialCSharp.Web.Extensions;
using EssentialCSharp.Web.Models;
using EssentialCSharp.Web.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace EssentialCSharp.Web.Extensions.Tests.Integration;
namespace EssentialCSharp.Web.Tests.Integration;

public class CaptchaTests(CaptchaServiceProvider serviceProvider) : IClassFixture<CaptchaServiceProvider>
[ClassDataSource<CaptchaServiceProvider>(Shared = SharedType.PerClass)]
public class CaptchaTests(CaptchaServiceProvider serviceProvider)
{
[Fact]
public async Task CaptchaService_Verify_Success()
[Test]
public async Task CaptchaService_Verify_Success(CancellationToken cancellationToken)
{
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);
HCaptchaResult? response = await captchaService.VerifyAsync(hCaptchaSecret, hCaptchaToken, hCaptchaSiteKey, cancellationToken);

Assert.NotNull(response);
Assert.True(response.Success);
await Assert.That(response).IsNotNull();
await Assert.That(response.Success).IsTrue();
}
}

public class CaptchaServiceProvider
public class CaptchaServiceProvider : IDisposable, IAsyncDisposable
{
public ServiceProvider ServiceProvider { get; } = CreateServiceProvider();
public static ServiceProvider CreateServiceProvider()
Expand All @@ -39,4 +41,24 @@ public static ServiceProvider CreateServiceProvider()

return services.BuildServiceProvider();
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (disposing)
{
ServiceProvider.Dispose();
}
}

public async ValueTask DisposeAsync()
{
await ServiceProvider.DisposeAsync().ConfigureAwait(false);
Dispose(disposing: false);
GC.SuppressFinalize(this);
}
}
Loading
Loading