Skip to content

Commit 11bcb4b

Browse files
Merge pull request #9 from HasanJaved-Developer/phase-5-webapp
Phase 5 webapp
2 parents 94d2321 + a540c84 commit 11bcb4b

File tree

11 files changed

+244
-6
lines changed

11 files changed

+244
-6
lines changed

.github/workflows/dotnet-tests.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: .NET Tests (collector)
2+
3+
on:
4+
push: { branches: [ "main" ] }
5+
pull_request: { branches: [ "main" ] }
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
- uses: actions/setup-dotnet@v4
13+
with:
14+
dotnet-version: '9.0.x'
15+
16+
- name: Restore
17+
run: dotnet restore
18+
19+
- name: Test with coverage (collector → LCOV)
20+
run: dotnet test --collect:"XPlat Code Coverage" --settings coverlet.runsettings --results-directory ./TestResults
21+
22+
# Upload all coverage.info files (supports multiple test projects)
23+
- name: Upload to Codecov
24+
uses: codecov/codecov-action@v4
25+
with:
26+
files: '**/TestResults/**/coverage.info'
27+
flags: unit,integration
28+
fail_ci_if_error: true
29+
verbose: true
30+
# token: ${{ secrets.CODECOV_TOKEN }} # only needed for private repos

CentralizedLoggingMonitoring.sln

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CentralizedLogging.Sdk", "C
1717
EndProject
1818
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CentralizedLogging.Contracts", "CentralizedLogging.Contracts\CentralizedLogging.Contracts.csproj", "{FAAAA797-C668-4B08-B5CA-504A21A3917A}"
1919
EndProject
20+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManagementApi.Tests.Unit", "UserManagementApi.Tests.Unit\UserManagementApi.Tests.Unit.csproj", "{66616F9C-B71C-45BF-8962-BAA356215BE6}"
21+
EndProject
22+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManagementApi.Tests.Integration", "UserManagementApi.Tests.Integration\UserManagementApi.Tests.Integration.csproj", "{DB6B7B64-C22D-474B-87B1-E48016C8633B}"
23+
EndProject
2024
Global
2125
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2226
Debug|Any CPU = Debug|Any CPU
@@ -51,6 +55,14 @@ Global
5155
{FAAAA797-C668-4B08-B5CA-504A21A3917A}.Debug|Any CPU.Build.0 = Debug|Any CPU
5256
{FAAAA797-C668-4B08-B5CA-504A21A3917A}.Release|Any CPU.ActiveCfg = Release|Any CPU
5357
{FAAAA797-C668-4B08-B5CA-504A21A3917A}.Release|Any CPU.Build.0 = Release|Any CPU
58+
{66616F9C-B71C-45BF-8962-BAA356215BE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
59+
{66616F9C-B71C-45BF-8962-BAA356215BE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
60+
{66616F9C-B71C-45BF-8962-BAA356215BE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
61+
{66616F9C-B71C-45BF-8962-BAA356215BE6}.Release|Any CPU.Build.0 = Release|Any CPU
62+
{DB6B7B64-C22D-474B-87B1-E48016C8633B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
63+
{DB6B7B64-C22D-474B-87B1-E48016C8633B}.Debug|Any CPU.Build.0 = Debug|Any CPU
64+
{DB6B7B64-C22D-474B-87B1-E48016C8633B}.Release|Any CPU.ActiveCfg = Release|Any CPU
65+
{DB6B7B64-C22D-474B-87B1-E48016C8633B}.Release|Any CPU.Build.0 = Release|Any CPU
5466
EndGlobalSection
5567
GlobalSection(SolutionProperties) = preSolution
5668
HideSolutionNode = FALSE

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Centralized Logging & Monitoring API
22

3+
[![Build](https://github.com/HasanJaved-Developer/CentralizedLoggingMonitoring/actions/workflows/dotnet-test-coverage.yml/badge.svg)](https://github.com/HasanJaved-Developer/CentralizedLoggingMonitoring/actions/workflows/dotnet-test-coverage.yml)
4+
[![Docker Build](https://github.com/HasanJaved-Developer/CentralizedLoggingMonitoring/actions/workflows/docker-build.yml/badge.svg)](https://github.com/HasanJaved-Developer/CentralizedLoggingMonitoring/actions/workflows/docker-build.yml)
5+
[![codecov](https://codecov.io/gh/HasanJaved-Developer/CentralizedLoggingMonitoring/branch/main/graph/badge.svg)](https://codecov.io/gh/HasanJaved-Developer/CentralizedLoggingMonitoring)
6+
[![License](https://img.shields.io/github/license/HasanJaved-Developer/CentralizedLoggingMonitoring)](./LICENSE.txt)
7+
![GitHub release (latest by date)](https://img.shields.io/github/v/release/HasanJaved-Developer/CentralizedLoggingMonitoring)
8+
9+
310
A centralized error logging and monitoring API built with **.NET 9**, Entity Framework Core, and SQL Server. This project is designed to serve as a foundation for collecting, storing, and managing error logs from multiple applications. **Phase 5,7&8** have also been complated and the pending **Phase 6** will be developed in a new repository.
411

512
Each phase of development is preserved in its own branch, culminating in the final **Integration Portal,** which, together with the API projects, forms a **Microservices-inspired architechture** that demonstrates **the Single Service Database Pattern.**
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using FluentAssertions;
2+
using Microsoft.AspNetCore.Mvc.Testing;
3+
using System.Net;
4+
using System.Net.Http.Json;
5+
using UserManagement.Contracts.Auth;
6+
7+
namespace UserManagementApi.Tests.Integration
8+
{
9+
public class LoginFlowTests : IClassFixture<WebApplicationFactory<Program>>
10+
{
11+
private readonly HttpClient _client;
12+
public LoginFlowTests(WebApplicationFactory<Program> factory)
13+
=> _client = factory.CreateClient();
14+
15+
[Fact]
16+
public async Task Login_ValidCredentials_Returns200AndToken()
17+
{
18+
var payload = new LoginRequest("bob","bob");
19+
var resp = await _client.PostAsJsonAsync("api/users/authenticate", payload);
20+
21+
resp.StatusCode.Should().Be(HttpStatusCode.OK);
22+
var body = await resp.Content.ReadFromJsonAsync<AuthResponse>();
23+
body.Should().NotBeNull();
24+
body.Token.Should().NotBeNullOrEmpty();
25+
}
26+
}
27+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="coverlet.collector" Version="6.0.2" />
12+
<PackageReference Include="coverlet.msbuild" Version="6.0.4">
13+
<PrivateAssets>all</PrivateAssets>
14+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
15+
</PackageReference>
16+
<PackageReference Include="FluentAssertions" Version="8.7.1" />
17+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.9" />
18+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
19+
<PackageReference Include="xunit" Version="2.9.2" />
20+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<ProjectReference Include="..\UserManagementApi\UserManagementApi.csproj" />
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<Using Include="Xunit" />
29+
</ItemGroup>
30+
31+
</Project>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using FluentAssertions;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.Extensions.Options;
6+
using Moq;
7+
using UserManagementApi.Data;
8+
using UserManagementApi.DTO;
9+
using UserManagementApi.DTO.Auth;
10+
11+
namespace UserManagementApi.Tests.Unit
12+
{
13+
public class LoginControllerTests
14+
{
15+
private static AppDbContext NewDb(string name)
16+
{
17+
var opts = new DbContextOptionsBuilder<AppDbContext>()
18+
.UseInMemoryDatabase(name)
19+
.Options;
20+
return new AppDbContext(opts);
21+
}
22+
23+
[Fact]
24+
public async Task Authenticate_WhenMissingUsernameOrPassword_Returns400WithValidationProblem()
25+
{
26+
using var db = NewDb(nameof(Authenticate_WhenMissingUsernameOrPassword_Returns400WithValidationProblem));
27+
var jwtOptions = new JwtOptions
28+
{
29+
Issuer = "test-issuer",
30+
Audience = "test-audience",
31+
Key = "supersecretkey1234567890"
32+
};
33+
var wrapped = Options.Create(jwtOptions); // returns IOptions<JwtOptions>
34+
35+
var controller = new TestableLoginController(db, wrapped);
36+
37+
var res = await controller.Authenticate(new LoginRequest("", ""));
38+
39+
var bad = res.Result.Should().BeOfType<BadRequestObjectResult>().Which;
40+
bad.StatusCode.Should().Be(StatusCodes.Status400BadRequest);
41+
bad.Value.Should().BeOfType<ValidationProblemDetails>();
42+
var v = (ValidationProblemDetails)bad.Value!;
43+
v.Errors.Should().ContainKey("userName");
44+
v.Errors.Should().ContainKey("password");
45+
}
46+
}
47+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Microsoft.Extensions.Options;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using UserManagementApi.Contracts.Models;
8+
using UserManagementApi.Controllers;
9+
using UserManagementApi.Data;
10+
using UserManagementApi.DTO;
11+
using UserManagementApi.DTO.Auth;
12+
13+
namespace UserManagementApi.Tests.Unit
14+
{
15+
public sealed class TestableLoginController :UsersController
16+
{
17+
private readonly string _jwtToReturn;
18+
private readonly DateTime _expToReturn;
19+
private readonly UserPermissionsDto _permToReturn;
20+
21+
public TestableLoginController(AppDbContext db,
22+
IOptions<JwtOptions> jwtopts)
23+
: base(db, jwtopts)
24+
{
25+
_jwtToReturn = "fake-jwt";
26+
_expToReturn = DateTime.UtcNow.AddHours(1);
27+
_permToReturn = new UserPermissionsDto(0, "abc", new List<CategoryDto>());
28+
29+
}
30+
protected override Task<UserPermissionsDto> BuildPermissionsForUser(int userId)
31+
=> Task.FromResult(_permToReturn);
32+
33+
protected override string GenerateJwt(AppUser user, List<CategoryDto> Categories, out DateTime expiresAtUtc)
34+
{
35+
expiresAtUtc = _expToReturn;
36+
return _jwtToReturn;
37+
}
38+
}
39+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="coverlet.collector" Version="6.0.2" />
12+
<PackageReference Include="coverlet.msbuild" Version="6.0.4">
13+
<PrivateAssets>all</PrivateAssets>
14+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
15+
</PackageReference>
16+
<PackageReference Include="FluentAssertions" Version="8.7.1" />
17+
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.9" />
18+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
19+
<PackageReference Include="Moq" Version="4.20.72" />
20+
<PackageReference Include="xunit" Version="2.9.2" />
21+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<ProjectReference Include="..\UserManagement.Contracts\UserManagement.Contracts.csproj" />
26+
<ProjectReference Include="..\UserManagementApi\UserManagementApi.csproj" />
27+
</ItemGroup>
28+
29+
<ItemGroup>
30+
<Using Include="Xunit" />
31+
</ItemGroup>
32+
33+
</Project>

UserManagementApi/Controllers/UsersController.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,7 @@ public async Task<ActionResult<AuthResponse>> Authenticate([FromBody] DTO.LoginR
7070
var dto = await BuildPermissionsForUser(user.Id);
7171

7272
var token = GenerateJwt(user, dto.Categories, out var expiresAtUtc);
73-
74-
// Get the same permissions tree you already expose
75-
var permissions = await BuildPermissionsForUser(user.Id);
76-
73+
7774
return Ok(new AuthResponse(user.Id, user.UserName, token, expiresAtUtc));
7875
}
7976

@@ -95,7 +92,7 @@ public async Task<ActionResult<UserPermissionsDto>> GetPermissions(int userId)
9592

9693
// ----- helpers -----
9794

98-
private async Task<UserPermissionsDto> BuildPermissionsForUser(int userId)
95+
protected virtual async Task<UserPermissionsDto> BuildPermissionsForUser(int userId)
9996
{
10097
// Fetch the user (for name in DTO)
10198
var user = await _db.Users.AsNoTracking().FirstAsync(u => u.Id == userId);
@@ -153,7 +150,7 @@ on rf.FunctionId equals f.Id
153150
return new UserPermissionsDto(user.Id, user.UserName, categoryDtos);
154151
}
155152

156-
private string GenerateJwt(AppUser user, List<CategoryDto> Categories, out DateTime expiresAtUtc)
153+
protected virtual string GenerateJwt(AppUser user, List<CategoryDto> Categories, out DateTime expiresAtUtc)
157154
{
158155
var keyBase64 = _jwt.Key;
159156
var keyPlain = Encoding.UTF8.GetString(Convert.FromBase64String(keyBase64));

UserManagementApi/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,4 @@ await app.MigrateAndSeedWithSqlLockAsync<AppDbContext>(
152152

153153

154154
app.Run();
155+
public partial class Program { } // make Program discoverable for tests

0 commit comments

Comments
 (0)