-
Notifications
You must be signed in to change notification settings - Fork 8
Iteration 1: Hello World works, button hidden for others #863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
edd4ef9
30402fa
41d9c03
bd8d5d4
17e3b38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| using System.Net; | ||
| using System.Net.Http.Json; | ||
| using EssentialCSharp.Web.Models; | ||
|
|
||
| namespace EssentialCSharp.Web.Tests; | ||
|
|
||
| public class ListingSourceCodeControllerTests | ||
| { | ||
| [Fact] | ||
| public async Task GetListing_WithValidChapterAndListing_Returns200WithContent() | ||
| { | ||
| // Arrange | ||
| using WebApplicationFactory factory = new(); | ||
| HttpClient client = factory.CreateClient(); | ||
|
|
||
| // Act | ||
| using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/1/1"); | ||
|
|
||
| // Assert | ||
| Assert.Equal(HttpStatusCode.OK, response.StatusCode); | ||
|
|
||
| ListingSourceCodeResponse? result = await response.Content.ReadFromJsonAsync<ListingSourceCodeResponse>(); | ||
| Assert.NotNull(result); | ||
| Assert.Equal(1, result.ChapterNumber); | ||
| Assert.Equal(1, result.ListingNumber); | ||
| Assert.NotEmpty(result.FileExtension); | ||
| Assert.NotEmpty(result.Content); | ||
| } | ||
|
|
||
|
|
||
| [Fact] | ||
| public async Task GetListing_WithInvalidChapter_Returns404() | ||
| { | ||
| // Arrange | ||
| using WebApplicationFactory factory = new(); | ||
| HttpClient client = factory.CreateClient(); | ||
|
|
||
| // Act | ||
| using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/999/1"); | ||
|
|
||
| // Assert | ||
| Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task GetListing_WithInvalidListing_Returns404() | ||
| { | ||
| // Arrange | ||
| using WebApplicationFactory factory = new(); | ||
| HttpClient client = factory.CreateClient(); | ||
|
|
||
| // Act | ||
| using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/1/999"); | ||
|
|
||
| // Assert | ||
| Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task GetListingsByChapter_WithValidChapter_ReturnsMultipleListings() | ||
| { | ||
| // Arrange | ||
| using WebApplicationFactory factory = new(); | ||
| HttpClient client = factory.CreateClient(); | ||
|
|
||
| // Act | ||
| using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/1"); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The api seems a bit arbitrary from the outside of /1/1 for example, maybe more like /chapter/1/listing/3 is clearer? @Keboo thoughts on this? Maybe it's not necessary since we only use it and it's the advent of ai
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea I think adding in a bit more context into the URL would be nice, just from a debugging perspective when looking at the logs. |
||
|
|
||
| // Assert | ||
| Assert.Equal(HttpStatusCode.OK, response.StatusCode); | ||
|
|
||
| List<ListingSourceCodeResponse>? results = await response.Content.ReadFromJsonAsync<List<ListingSourceCodeResponse>>(); | ||
| Assert.NotNull(results); | ||
| Assert.NotEmpty(results); | ||
|
|
||
| // Verify all results are from chapter 1 | ||
| Assert.All(results, r => Assert.Equal(1, r.ChapterNumber)); | ||
|
|
||
| // Verify results are ordered by listing number | ||
| Assert.Equal(results.OrderBy(r => r.ListingNumber).ToList(), results); | ||
|
|
||
| // Verify each listing has required properties | ||
| Assert.All(results, r => | ||
| { | ||
| Assert.NotEmpty(r.FileExtension); | ||
| Assert.NotEmpty(r.Content); | ||
| }); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task GetListingsByChapter_WithInvalidChapter_ReturnsEmptyList() | ||
| { | ||
| // Arrange | ||
| using WebApplicationFactory factory = new(); | ||
| HttpClient client = factory.CreateClient(); | ||
|
|
||
| // Act | ||
| using HttpResponseMessage response = await client.GetAsync("/api/ListingSourceCode/999"); | ||
|
|
||
| // Assert | ||
| Assert.Equal(HttpStatusCode.OK, response.StatusCode); | ||
|
|
||
| List<ListingSourceCodeResponse>? results = await response.Content.ReadFromJsonAsync<List<ListingSourceCodeResponse>>(); | ||
| Assert.NotNull(results); | ||
| Assert.Empty(results); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| using EssentialCSharp.Web.Models; | ||
| using EssentialCSharp.Web.Services; | ||
| using Microsoft.AspNetCore.Hosting; | ||
| using Microsoft.Extensions.FileProviders; | ||
| using Microsoft.Extensions.Logging; | ||
| using Moq; | ||
|
|
||
| namespace EssentialCSharp.Web.Tests; | ||
|
|
||
| public class ListingSourceCodeServiceTests | ||
| { | ||
| [Fact] | ||
| public async Task GetListingAsync_WithValidChapterAndListing_ReturnsCorrectListing() | ||
| { | ||
| // Arrange | ||
| ListingSourceCodeService service = CreateService(); | ||
|
|
||
| // Act | ||
| ListingSourceCodeResponse? result = await service.GetListingAsync(1, 1); | ||
|
|
||
| // Assert | ||
| Assert.NotNull(result); | ||
| Assert.Equal(1, result.ChapterNumber); | ||
| Assert.Equal(1, result.ListingNumber); | ||
| Assert.Equal("cs", result.FileExtension); | ||
| Assert.NotEmpty(result.Content); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task GetListingAsync_WithInvalidChapter_ReturnsNull() | ||
| { | ||
| // Arrange | ||
| ListingSourceCodeService service = CreateService(); | ||
|
|
||
| // Act | ||
| ListingSourceCodeResponse? result = await service.GetListingAsync(999, 1); | ||
|
|
||
| // Assert | ||
| Assert.Null(result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task GetListingAsync_WithInvalidListing_ReturnsNull() | ||
| { | ||
| // Arrange | ||
| ListingSourceCodeService service = CreateService(); | ||
|
|
||
| // Act | ||
| ListingSourceCodeResponse? result = await service.GetListingAsync(1, 999); | ||
|
|
||
| // Assert | ||
| Assert.Null(result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task GetListingAsync_DifferentFileExtension_AutoDiscoversFileExtension() | ||
| { | ||
| // Arrange | ||
| ListingSourceCodeService service = CreateService(); | ||
|
|
||
| // Act - Get an XML file (01.02.xml exists in Chapter 1) | ||
| ListingSourceCodeResponse? result = await service.GetListingAsync(1, 2); | ||
|
|
||
| // Assert | ||
| Assert.NotNull(result); | ||
| Assert.Equal("xml", result.FileExtension); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task GetListingsByChapterAsync_WithValidChapter_ReturnsAllListings() | ||
| { | ||
| // Arrange | ||
| ListingSourceCodeService service = CreateService(); | ||
|
|
||
| // Act | ||
| IReadOnlyList<ListingSourceCodeResponse> results = await service.GetListingsByChapterAsync(1); | ||
|
|
||
| // Assert | ||
| Assert.NotEmpty(results); | ||
| Assert.All(results, r => Assert.Equal(1, r.ChapterNumber)); | ||
| Assert.All(results, r => Assert.NotEmpty(r.Content)); | ||
| Assert.All(results, r => Assert.NotEmpty(r.FileExtension)); | ||
|
|
||
| // Verify results are ordered | ||
| Assert.Equal(results.OrderBy(r => r.ListingNumber).ToList(), results); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task GetListingsByChapterAsync_DirectoryContainsNonListingFiles_ExcludesNonListingFiles() | ||
| { | ||
| // Arrange - Chapter 10 has Employee.cs which doesn't match the pattern | ||
| ListingSourceCodeService service = CreateService(); | ||
|
|
||
| // Act | ||
| IReadOnlyList<ListingSourceCodeResponse> results = await service.GetListingsByChapterAsync(10); | ||
|
|
||
| // Assert | ||
| Assert.NotEmpty(results); | ||
|
|
||
| // Ensure all results match the {CC}.{LL}.{ext} pattern | ||
| Assert.All(results, r => | ||
| { | ||
| Assert.Equal(10, r.ChapterNumber); | ||
| Assert.InRange(r.ListingNumber, 1, 99); | ||
| }); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task GetListingsByChapterAsync_WithInvalidChapter_ReturnsEmptyList() | ||
| { | ||
| // Arrange | ||
| ListingSourceCodeService service = CreateService(); | ||
|
|
||
| // Act | ||
| IReadOnlyList<ListingSourceCodeResponse> results = await service.GetListingsByChapterAsync(999); | ||
|
|
||
| // Assert | ||
| Assert.Empty(results); | ||
| } | ||
|
|
||
| private static ListingSourceCodeService CreateService() | ||
| { | ||
| string testDataRoot = GetTestDataPath(); | ||
|
|
||
| var mockWebHostEnvironment = new Mock<IWebHostEnvironment>(); | ||
| mockWebHostEnvironment.Setup(m => m.ContentRootPath).Returns(testDataRoot); | ||
| mockWebHostEnvironment.Setup(m => m.ContentRootFileProvider).Returns(new PhysicalFileProvider(testDataRoot)); | ||
|
|
||
| var mockLogger = new Mock<ILogger<ListingSourceCodeService>>(); | ||
|
|
||
| return new ListingSourceCodeService(mockWebHostEnvironment.Object, mockLogger.Object); | ||
|
Comment on lines
+125
to
+131
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a case where Moq.AutoMock would be helpful as it would decouple this from the constructor. Let me know if you struggle to use it. |
||
| } | ||
|
|
||
| private static string GetTestDataPath() | ||
| { | ||
| // Get the test project directory and navigate to TestData folder | ||
| string currentDirectory = Directory.GetCurrentDirectory(); | ||
| string testDataPath = Path.Combine(currentDirectory, "TestData"); | ||
|
|
||
| if (!Directory.Exists(testDataPath)) | ||
| { | ||
| throw new InvalidOperationException($"TestData directory not found at: {testDataPath}"); | ||
| } | ||
|
|
||
| return testDataPath; | ||
| } | ||
|
Comment on lines
+134
to
+146
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A couple things here. When working with the file system it is better to return the stronger typed objects rather than strings. So in this case returning I am a little curious how |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| // Test listing 01.01 | ||
| using System; | ||
|
|
||
| class Program | ||
| { | ||
| static void Main() | ||
| { | ||
| Console.WriteLine("Hello, World!"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <!-- Test XML listing 01.02 --> | ||
| <configuration> | ||
| <appSettings> | ||
| <add key="TestKey" value="TestValue" /> | ||
| </appSettings> | ||
| </configuration> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| // Test listing 01.03 | ||
| namespace TestNamespace | ||
| { | ||
| public class TestClass | ||
| { | ||
| public int TestProperty { get; set; } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| // Test listing 10.01 | ||
| public class Employee | ||
| { | ||
| public string Name { get; set; } | ||
| public int Id { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| // Test listing 10.02 | ||
| public class Manager : Employee | ||
| { | ||
| public List<Employee> DirectReports { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| // This file should NOT be picked up by the listing pattern | ||
| // It doesn't match {CC}.{LL}.{ext} format | ||
| public class EmployeeHelper | ||
| { | ||
| public static void DoSomething() { } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Keboo is always sad to not see AutoMoq ;)