Skip to content

Commit edd4ef9

Browse files
Implement ListingSourceCode feature with controller, service, and tests
1 parent 2d71bce commit edd4ef9

17 files changed

Lines changed: 538 additions & 0 deletions

.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/

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" />
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/1/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/999/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/1/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/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/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 Microsoft.Extensions.Logging;
6+
using Moq;
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+
string testDataRoot = GetTestDataPath();
124+
125+
var mockWebHostEnvironment = new Mock<IWebHostEnvironment>();
126+
mockWebHostEnvironment.Setup(m => m.ContentRootPath).Returns(testDataRoot);
127+
mockWebHostEnvironment.Setup(m => m.ContentRootFileProvider).Returns(new PhysicalFileProvider(testDataRoot));
128+
129+
var mockLogger = new Mock<ILogger<ListingSourceCodeService>>();
130+
131+
return new ListingSourceCodeService(mockWebHostEnvironment.Object, mockLogger.Object);
132+
}
133+
134+
private static string GetTestDataPath()
135+
{
136+
// Get the test project directory and navigate to TestData folder
137+
string currentDirectory = Directory.GetCurrentDirectory();
138+
string testDataPath = Path.Combine(currentDirectory, "TestData");
139+
140+
if (!Directory.Exists(testDataPath))
141+
{
142+
throw new InvalidOperationException($"TestData directory not found at: {testDataPath}");
143+
}
144+
145+
return testDataPath;
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+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Test listing 10.02
2+
public class Manager : Employee
3+
{
4+
public List<Employee> DirectReports { get; set; }
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// This file should NOT be picked up by the listing pattern
2+
// It doesn't match {CC}.{LL}.{ext} format
3+
public class EmployeeHelper
4+
{
5+
public static void DoSomething() { }
6+
}

0 commit comments

Comments
 (0)