Skip to content

Commit c883a07

Browse files
Iteration 1: Hello World works, button hidden for others (#863)
## Description Adds api controller to access the code listings pulled from the private nuget feed. Added try .NET functionality to access IntelliTect Try Containerapp and added interactive code execution window. ### Ensure that your pull request has followed all the steps below: - [ ] Code compilation - [ ] Created tests which fail without the change (if possible) - [ ] All tests passing - [ ] Extended the README / documentation, if necessary --------- Co-authored-by: Kevin B <Keboo@users.noreply.github.com>
1 parent c92b416 commit c883a07

25 files changed

+1997
-4
lines changed

.github/workflows/Build-Test-And-Deploy.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ jobs:
138138
ACR_USERNAME: ${{ secrets.ESSENTIALCSHARP_ACR_USERNAME }}
139139
ACR_PASSWORD: ${{ secrets.ESSENTIALCSHARP_ACR_PASSWORD }}
140140
AZURECLIENTID: ${{ secrets.IDENTITY_CLIENT_ID }}
141+
TRYDOTNET_ORIGIN: ${{ vars.TRYDOTNET_ORIGIN }}
141142
with:
142143
inlineScript: |
143144
az containerapp identity assign -n ${{ vars.CONTAINER_APP_NAME }} -g ${{ vars.RESOURCEGROUP }} --user-assigned ${{ vars.CONTAINER_APP_IDENTITY }}
@@ -157,7 +158,8 @@ jobs:
157158
AuthMessageSender__SendFromName=secretref:emailsender-name AuthMessageSender__SendFromEmail=secretref:emailsender-email ConnectionStrings__EssentialCSharpWebContextConnection=secretref:connectionstring ASPNETCORE_ENVIRONMENT=Staging \
158159
AZURE_CLIENT_ID=$AZURECLIENTID HCaptcha__SiteKey=secretref:captcha-sitekey HCaptcha__SecretKey=secretref:captcha-secretkey ApplicationInsights__ConnectionString=secretref:appinsights-connectionstring \
159160
AIOptions__Endpoint=secretref:ai-endpoint AIOptions__ApiKey=secretref:ai-apikey AIOptions__VectorGenerationDeploymentName=secretref:ai-vectordeployment AIOptions__ChatDeploymentName=secretref:ai-chatdeployment \
160-
AIOptions__SystemPrompt=secretref:ai-systemprompt ConnectionStrings__PostgresVectorStore=secretref:postgres-vectorstore-connectionstring
161+
AIOptions__SystemPrompt=secretref:ai-systemprompt ConnectionStrings__PostgresVectorStore=secretref:postgres-vectorstore-connectionstring \
162+
TryDotNet__Origin=$TRYDOTNET_ORIGIN
161163
162164
- name: Logout of Azure CLI
163165
if: always()
@@ -230,6 +232,7 @@ jobs:
230232
ACR_USERNAME: ${{ secrets.ESSENTIALCSHARP_ACR_USERNAME }}
231233
ACR_PASSWORD: ${{ secrets.ESSENTIALCSHARP_ACR_PASSWORD }}
232234
AZURECLIENTID: ${{ secrets.IDENTITY_CLIENT_ID }}
235+
TRYDOTNET_ORIGIN: ${{ vars.TRYDOTNET_ORIGIN }}
233236
with:
234237
inlineScript: |
235238
az containerapp identity assign -n ${{ vars.CONTAINER_APP_NAME }} -g ${{ vars.RESOURCEGROUP }} --user-assigned ${{ vars.CONTAINER_APP_IDENTITY }}
@@ -249,7 +252,8 @@ jobs:
249252
AuthMessageSender__SendFromName=secretref:emailsender-name AuthMessageSender__SendFromEmail=secretref:emailsender-email ConnectionStrings__EssentialCSharpWebContextConnection=secretref:connectionstring ASPNETCORE_ENVIRONMENT=Production \
250253
AZURE_CLIENT_ID=$AZURECLIENTID HCaptcha__SiteKey=secretref:captcha-sitekey HCaptcha__SecretKey=secretref:captcha-secretkey ApplicationInsights__ConnectionString=secretref:appinsights-connectionstring \
251254
AIOptions__Endpoint=secretref:ai-endpoint AIOptions__ApiKey=secretref:ai-apikey AIOptions__VectorGenerationDeploymentName=secretref:ai-vectordeployment AIOptions__ChatDeploymentName=secretref:ai-chatdeployment \
252-
AIOptions__SystemPrompt=secretref:ai-systemprompt ConnectionStrings__PostgresVectorStore=secretref:postgres-vectorstore-connectionstring
255+
AIOptions__SystemPrompt=secretref:ai-systemprompt ConnectionStrings__PostgresVectorStore=secretref:postgres-vectorstore-connectionstring \
256+
TryDotNet__Origin=$TRYDOTNET_ORIGIN
253257
254258
255259
- name: Logout of Azure CLI

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ wwwroot/Chapters
3535
EssentialCSharp.Web/wwwroot/Chapters
3636
EssentialCSharp.Web/wwwroot/sitemap.xml
3737
EssentialCSharp.Web/Chapters/
38+
EssentialCSharp.Web/ListingSourceCode
3839
Utilities/EssentialCSharp.Web/Chapters/
3940
Utilities/EssentialCSharp.Web/wwwroot/sitemap.xml
4041
Utilities/EssentialCSharp.Web/wwwroot/Chapters/

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<PackageVersion Include="ModelContextProtocol" Version="0.3.0-preview.4" />
4646
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="0.3.0-preview.4" />
4747
<PackageVersion Include="Moq" Version="4.20.72" />
48+
<PackageVersion Include="Moq.AutoMock" Version="3.6.1" />
4849
<PackageVersion Include="System.CommandLine" Version="2.0.3" />
4950
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
5051
<PackageVersion Include="Octokit" Version="14.0.0" />

EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
1616
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
1717
<PackageReference Include="Microsoft.NET.Test.Sdk" />
18+
<PackageReference Include="Moq.AutoMock" />
1819
<PackageReference Include="Newtonsoft.Json" />
1920
<PackageReference Include="xunit" />
2021
<PackageReference Include="xunit.runner.visualstudio">
@@ -35,4 +36,15 @@
3536
<ProjectReference Include="..\EssentialCSharp.Web\EssentialCSharp.Web.csproj" />
3637
</ItemGroup>
3738

39+
<ItemGroup>
40+
<!-- Exclude test data files from compilation -->
41+
<Compile Remove="TestData/**" />
42+
<EmbeddedResource Remove="TestData/**" />
43+
44+
<!-- Explicitly include all test data files as content to copy to output -->
45+
<Content Include="TestData/**">
46+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
47+
</Content>
48+
</ItemGroup>
49+
3850
</Project>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using System.Net;
2+
using System.Net.Http.Json;
3+
using EssentialCSharp.Web.Models;
4+
5+
namespace EssentialCSharp.Web.Tests;
6+
7+
public class ListingSourceCodeControllerTests
8+
{
9+
[Fact]
10+
public async Task GetListing_WithValidChapterAndListing_Returns200WithContent()
11+
{
12+
// Arrange
13+
using WebApplicationFactory factory = new();
14+
HttpClient client = factory.CreateClient();
15+
16+
// Act
17+
using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/chapter/1/listing/1");
18+
19+
// Assert
20+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
21+
22+
ListingSourceCodeResponse? result = await response.Content.ReadFromJsonAsync<ListingSourceCodeResponse>();
23+
Assert.NotNull(result);
24+
Assert.Equal(1, result.ChapterNumber);
25+
Assert.Equal(1, result.ListingNumber);
26+
Assert.NotEmpty(result.FileExtension);
27+
Assert.NotEmpty(result.Content);
28+
}
29+
30+
31+
[Fact]
32+
public async Task GetListing_WithInvalidChapter_Returns404()
33+
{
34+
// Arrange
35+
using WebApplicationFactory factory = new();
36+
HttpClient client = factory.CreateClient();
37+
38+
// Act
39+
using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/chapter/999/listing/1");
40+
41+
// Assert
42+
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
43+
}
44+
45+
[Fact]
46+
public async Task GetListing_WithInvalidListing_Returns404()
47+
{
48+
// Arrange
49+
using WebApplicationFactory factory = new();
50+
HttpClient client = factory.CreateClient();
51+
52+
// Act
53+
using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/chapter/1/listing/999");
54+
55+
// Assert
56+
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
57+
}
58+
59+
[Fact]
60+
public async Task GetListingsByChapter_WithValidChapter_ReturnsMultipleListings()
61+
{
62+
// Arrange
63+
using WebApplicationFactory factory = new();
64+
HttpClient client = factory.CreateClient();
65+
66+
// Act
67+
using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/chapter/1");
68+
69+
// Assert
70+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
71+
72+
List<ListingSourceCodeResponse>? results = await response.Content.ReadFromJsonAsync<List<ListingSourceCodeResponse>>();
73+
Assert.NotNull(results);
74+
Assert.NotEmpty(results);
75+
76+
// Verify all results are from chapter 1
77+
Assert.All(results, r => Assert.Equal(1, r.ChapterNumber));
78+
79+
// Verify results are ordered by listing number
80+
Assert.Equal(results.OrderBy(r => r.ListingNumber).ToList(), results);
81+
82+
// Verify each listing has required properties
83+
Assert.All(results, r =>
84+
{
85+
Assert.NotEmpty(r.FileExtension);
86+
Assert.NotEmpty(r.Content);
87+
});
88+
}
89+
90+
[Fact]
91+
public async Task GetListingsByChapter_WithInvalidChapter_ReturnsEmptyList()
92+
{
93+
// Arrange
94+
using WebApplicationFactory factory = new();
95+
HttpClient client = factory.CreateClient();
96+
97+
// Act
98+
using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/chapter/999");
99+
100+
// Assert
101+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
102+
103+
List<ListingSourceCodeResponse>? results = await response.Content.ReadFromJsonAsync<List<ListingSourceCodeResponse>>();
104+
Assert.NotNull(results);
105+
Assert.Empty(results);
106+
}
107+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
using EssentialCSharp.Web.Models;
2+
using EssentialCSharp.Web.Services;
3+
using Microsoft.AspNetCore.Hosting;
4+
using Microsoft.Extensions.FileProviders;
5+
using Moq;
6+
using Moq.AutoMock;
7+
8+
namespace EssentialCSharp.Web.Tests;
9+
10+
public class ListingSourceCodeServiceTests
11+
{
12+
[Fact]
13+
public async Task GetListingAsync_WithValidChapterAndListing_ReturnsCorrectListing()
14+
{
15+
// Arrange
16+
ListingSourceCodeService service = CreateService();
17+
18+
// Act
19+
ListingSourceCodeResponse? result = await service.GetListingAsync(1, 1);
20+
21+
// Assert
22+
Assert.NotNull(result);
23+
Assert.Equal(1, result.ChapterNumber);
24+
Assert.Equal(1, result.ListingNumber);
25+
Assert.Equal("cs", result.FileExtension);
26+
Assert.NotEmpty(result.Content);
27+
}
28+
29+
[Fact]
30+
public async Task GetListingAsync_WithInvalidChapter_ReturnsNull()
31+
{
32+
// Arrange
33+
ListingSourceCodeService service = CreateService();
34+
35+
// Act
36+
ListingSourceCodeResponse? result = await service.GetListingAsync(999, 1);
37+
38+
// Assert
39+
Assert.Null(result);
40+
}
41+
42+
[Fact]
43+
public async Task GetListingAsync_WithInvalidListing_ReturnsNull()
44+
{
45+
// Arrange
46+
ListingSourceCodeService service = CreateService();
47+
48+
// Act
49+
ListingSourceCodeResponse? result = await service.GetListingAsync(1, 999);
50+
51+
// Assert
52+
Assert.Null(result);
53+
}
54+
55+
[Fact]
56+
public async Task GetListingAsync_DifferentFileExtension_AutoDiscoversFileExtension()
57+
{
58+
// Arrange
59+
ListingSourceCodeService service = CreateService();
60+
61+
// Act - Get an XML file (01.02.xml exists in Chapter 1)
62+
ListingSourceCodeResponse? result = await service.GetListingAsync(1, 2);
63+
64+
// Assert
65+
Assert.NotNull(result);
66+
Assert.Equal("xml", result.FileExtension);
67+
}
68+
69+
[Fact]
70+
public async Task GetListingsByChapterAsync_WithValidChapter_ReturnsAllListings()
71+
{
72+
// Arrange
73+
ListingSourceCodeService service = CreateService();
74+
75+
// Act
76+
IReadOnlyList<ListingSourceCodeResponse> results = await service.GetListingsByChapterAsync(1);
77+
78+
// Assert
79+
Assert.NotEmpty(results);
80+
Assert.All(results, r => Assert.Equal(1, r.ChapterNumber));
81+
Assert.All(results, r => Assert.NotEmpty(r.Content));
82+
Assert.All(results, r => Assert.NotEmpty(r.FileExtension));
83+
84+
// Verify results are ordered
85+
Assert.Equal(results.OrderBy(r => r.ListingNumber).ToList(), results);
86+
}
87+
88+
[Fact]
89+
public async Task GetListingsByChapterAsync_DirectoryContainsNonListingFiles_ExcludesNonListingFiles()
90+
{
91+
// Arrange - Chapter 10 has Employee.cs which doesn't match the pattern
92+
ListingSourceCodeService service = CreateService();
93+
94+
// Act
95+
IReadOnlyList<ListingSourceCodeResponse> results = await service.GetListingsByChapterAsync(10);
96+
97+
// Assert
98+
Assert.NotEmpty(results);
99+
100+
// Ensure all results match the {CC}.{LL}.{ext} pattern
101+
Assert.All(results, r =>
102+
{
103+
Assert.Equal(10, r.ChapterNumber);
104+
Assert.InRange(r.ListingNumber, 1, 99);
105+
});
106+
}
107+
108+
[Fact]
109+
public async Task GetListingsByChapterAsync_WithInvalidChapter_ReturnsEmptyList()
110+
{
111+
// Arrange
112+
ListingSourceCodeService service = CreateService();
113+
114+
// Act
115+
IReadOnlyList<ListingSourceCodeResponse> results = await service.GetListingsByChapterAsync(999);
116+
117+
// Assert
118+
Assert.Empty(results);
119+
}
120+
121+
private static ListingSourceCodeService CreateService()
122+
{
123+
DirectoryInfo testDataRoot = GetTestDataPath();
124+
125+
AutoMocker mocker = new();
126+
Mock<IWebHostEnvironment> mockWebHostEnvironment = mocker.GetMock<IWebHostEnvironment>();
127+
mockWebHostEnvironment.Setup(m => m.ContentRootPath).Returns(testDataRoot.FullName);
128+
mockWebHostEnvironment.Setup(m => m.ContentRootFileProvider).Returns(new PhysicalFileProvider(testDataRoot.FullName));
129+
130+
return mocker.CreateInstance<ListingSourceCodeService>();
131+
}
132+
133+
private static DirectoryInfo GetTestDataPath()
134+
{
135+
string baseDirectory = AppContext.BaseDirectory;
136+
string testDataPath = Path.Combine(baseDirectory, "TestData");
137+
138+
DirectoryInfo testDataDirectory = new(testDataPath);
139+
140+
if (!testDataDirectory.Exists)
141+
{
142+
throw new InvalidOperationException($"TestData directory not found at: {testDataDirectory.FullName}");
143+
}
144+
145+
return testDataDirectory;
146+
}
147+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Test listing 01.01
2+
using System;
3+
4+
class Program
5+
{
6+
static void Main()
7+
{
8+
Console.WriteLine("Hello, World!");
9+
}
10+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- Test XML listing 01.02 -->
3+
<configuration>
4+
<appSettings>
5+
<add key="TestKey" value="TestValue" />
6+
</appSettings>
7+
</configuration>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Test listing 01.03
2+
namespace TestNamespace
3+
{
4+
public class TestClass
5+
{
6+
public int TestProperty { get; set; }
7+
}
8+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Test listing 10.01
2+
public class Employee
3+
{
4+
public string Name { get; set; }
5+
public int Id { get; set; }
6+
}

0 commit comments

Comments
 (0)