Skip to content

Commit f03b005

Browse files
Merge pull request #706 from PathfinderHonorManager/develop
Fixed swagger authz issues
2 parents 6876adc + d67241c commit f03b005

17 files changed

+1153
-15
lines changed

.github/workflows/dependabot_automerge.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
steps:
1313
- name: Dependabot metadata
1414
id: metadata
15-
uses: dependabot/fetch-metadata@v2.4.0
15+
uses: dependabot/fetch-metadata@v2.5.0
1616
with:
1717
github-token: "${{ secrets.GITHUB_TOKEN }}"
1818
- name: Enable auto-merge for Dependabot PRs

AGENTS.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
- `PathfinderHonorManager/` hosts the ASP.NET Core API. Key areas: `Controllers/`, `Service/`, `DataAccess/`, `Model/`, `Dto/`, `Validators/`, `Healthcheck/`, `Migrations/`, `Swagger/`, `Mapping/`, and `Converters/`.
5+
- `PathfinderHonorManager.Tests/` contains NUnit tests plus test helpers and seeders.
6+
- `PathfinderHonorManager/Pathfinder-DB/` stores database SQL scripts.
7+
- Configuration lives in `PathfinderHonorManager/appsettings.json`, `PathfinderHonorManager/appsettings.Development.json`, and `PathfinderHonorManager/Properties/launchSettings.json`.
8+
9+
## Build, Test, and Development Commands
10+
- `dotnet build PathfinderHonorManager.sln` builds the solution.
11+
- `dotnet run --project PathfinderHonorManager` runs the API locally.
12+
- `dotnet test PathfinderHonorManager.Tests/PathfinderHonorManager.Tests.csproj` runs the test suite.
13+
- `dotnet ef migrations add DescriptiveMigrationName --project PathfinderHonorManager` creates a migration.
14+
- `dotnet ef database update --project PathfinderHonorManager` applies migrations (see `EF_MIGRATIONS_README.md` for baseline setup).
15+
16+
## Coding Style & Naming Conventions
17+
- Follow C# conventions: 4-space indentation; PascalCase for types and public members; camelCase for locals and parameters.
18+
- Interfaces use the `I*` prefix (for example, `IHonorService`).
19+
- File/class naming mirrors feature type: `*Controller`, `*Service`, `*Validator`, `*Dto`.
20+
- Validators use FluentValidation and live in `PathfinderHonorManager/Validators`.
21+
22+
## Testing Guidelines
23+
- NUnit is the primary framework; tests live in `PathfinderHonorManager.Tests` and use `[Test]`/`[TestCase]`.
24+
- Name test files and classes with a `*Tests` suffix (for example, `HonorsControllerTests.cs`).
25+
- Prefer in-memory EF providers or seeded helpers (`Helpers/DatabaseSeeder.cs`) for data setup.
26+
27+
## Commit & Pull Request Guidelines
28+
- Recent commits use short, imperative sentence-case summaries (for example, "Fix SonarQube issues", "Add background worker...").
29+
- Keep commits focused on a single change set.
30+
- PRs should include: a clear description, testing notes/commands run, migration impacts (if any), and any required configuration or env var changes.
31+
32+
## Configuration & Migrations
33+
- Required env vars include `PathfinderCS` and Auth0 settings (`Auth0:Domain`, `Auth0:Audience`, `Auth0:ClientId`).
34+
- The baseline migration is tracked in `PathfinderHonorManager/Migrations`; follow `EF_MIGRATIONS_README.md` for local setup and safe rollback steps.
35+
- Health endpoints are available at `/health`, `/health/ready`, and `/health/live`.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using System.Security.Claims;
3+
using System.Threading.Tasks;
4+
using Microsoft.AspNetCore.Authorization;
5+
using NUnit.Framework;
6+
using PathfinderHonorManager.Auth;
7+
8+
namespace PathfinderHonorManager.Tests.Auth
9+
{
10+
[TestFixture]
11+
public class HasScopeHandlerTests
12+
{
13+
[Test]
14+
public async Task HandleAsync_NoPermissionsClaim_DoesNotSucceed()
15+
{
16+
var requirement = new HasScopeRequirement("read:honors", "https://issuer/");
17+
var user = new ClaimsPrincipal(new ClaimsIdentity());
18+
var context = new AuthorizationHandlerContext(new[] { requirement }, user, null);
19+
var handler = new HasScopeHandler();
20+
21+
await handler.HandleAsync(context);
22+
23+
Assert.That(context.HasSucceeded, Is.False);
24+
}
25+
26+
[Test]
27+
public async Task HandleAsync_PermissionsClaimWithMatchingScope_Succeeds()
28+
{
29+
var requirement = new HasScopeRequirement("read:honors", "https://issuer/");
30+
var identity = new ClaimsIdentity(new[]
31+
{
32+
new Claim("permissions", "read:honors", ClaimValueTypes.String, "https://issuer/")
33+
});
34+
var user = new ClaimsPrincipal(identity);
35+
var context = new AuthorizationHandlerContext(new[] { requirement }, user, null);
36+
var handler = new HasScopeHandler();
37+
38+
await handler.HandleAsync(context);
39+
40+
Assert.That(context.HasSucceeded, Is.True);
41+
}
42+
43+
[Test]
44+
public async Task HandleAsync_PermissionsClaimWithWrongIssuer_DoesNotSucceed()
45+
{
46+
var requirement = new HasScopeRequirement("read:honors", "https://issuer/");
47+
var identity = new ClaimsIdentity(new[]
48+
{
49+
new Claim("permissions", "read:honors", ClaimValueTypes.String, "https://other/")
50+
});
51+
var user = new ClaimsPrincipal(identity);
52+
var context = new AuthorizationHandlerContext(new[] { requirement }, user, null);
53+
var handler = new HasScopeHandler();
54+
55+
await handler.HandleAsync(context);
56+
57+
Assert.That(context.HasSucceeded, Is.False);
58+
}
59+
60+
[Test]
61+
public void Constructor_NullScope_Throws()
62+
{
63+
var ex = Assert.Throws<ArgumentNullException>(() => new HasScopeRequirement(null, "https://issuer/"));
64+
Assert.That(ex!.ParamName, Is.EqualTo("scope"));
65+
}
66+
67+
[Test]
68+
public void Constructor_NullIssuer_Throws()
69+
{
70+
var ex = Assert.Throws<ArgumentNullException>(() => new HasScopeRequirement("read:honors", null));
71+
Assert.That(ex!.ParamName, Is.EqualTo("issuer"));
72+
}
73+
}
74+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using Microsoft.Data.Sqlite;
4+
using Microsoft.EntityFrameworkCore;
5+
using NUnit.Framework;
6+
using PathfinderHonorManager.DataAccess;
7+
using PathfinderHonorManager.Healthcheck;
8+
9+
namespace PathfinderHonorManager.Tests.Healthcheck
10+
{
11+
[TestFixture]
12+
public class MigrationHealthCheckTests
13+
{
14+
[Test]
15+
public async Task CheckHealthAsync_PendingMigrations_ReturnsDegradedOrHealthy()
16+
{
17+
using var connection = new SqliteConnection("DataSource=:memory:");
18+
connection.Open();
19+
20+
var options = new DbContextOptionsBuilder<PathfinderContext>()
21+
.UseSqlite(connection)
22+
.Options;
23+
24+
using var context = new PathfinderContext(options);
25+
var healthCheck = new MigrationHealthCheck(context);
26+
27+
var result = await healthCheck.CheckHealthAsync(
28+
new Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckContext(),
29+
CancellationToken.None);
30+
31+
Assert.That(result.Status, Is.Not.EqualTo(
32+
Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Unhealthy));
33+
}
34+
35+
[Test]
36+
public async Task CheckHealthAsync_DisposedContext_ReturnsUnhealthy()
37+
{
38+
var options = new DbContextOptionsBuilder<PathfinderContext>()
39+
.UseSqlite("DataSource=:memory:")
40+
.Options;
41+
42+
var context = new PathfinderContext(options);
43+
context.Dispose();
44+
45+
var healthCheck = new MigrationHealthCheck(context);
46+
var result = await healthCheck.CheckHealthAsync(
47+
new Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckContext(),
48+
CancellationToken.None);
49+
50+
Assert.That(result.Status, Is.EqualTo(
51+
Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Unhealthy));
52+
}
53+
}
54+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Hosting;
6+
using Microsoft.AspNetCore.Mvc.Testing;
7+
using Microsoft.Data.Sqlite;
8+
using Microsoft.EntityFrameworkCore;
9+
using Microsoft.EntityFrameworkCore.Infrastructure;
10+
using Microsoft.EntityFrameworkCore.Storage;
11+
using Microsoft.Extensions.Configuration;
12+
using Microsoft.Extensions.DependencyInjection;
13+
using Microsoft.Extensions.DependencyInjection.Extensions;
14+
using Microsoft.Extensions.Hosting;
15+
using PathfinderHonorManager;
16+
using PathfinderHonorManager.DataAccess;
17+
using PathfinderHonorManager.Service;
18+
19+
namespace PathfinderHonorManager.Tests.Integration
20+
{
21+
public class IntegrationTestWebAppFactory : WebApplicationFactory<Startup>
22+
{
23+
private readonly SqliteConnection _connection;
24+
25+
private readonly IReadOnlyCollection<string> _permissions;
26+
27+
public IntegrationTestWebAppFactory(IReadOnlyCollection<string> permissions = null)
28+
{
29+
_permissions = permissions ?? TestAuthHandler.DefaultPermissions;
30+
_connection = new SqliteConnection("DataSource=:memory:");
31+
_connection.Open();
32+
}
33+
34+
protected override void ConfigureWebHost(IWebHostBuilder builder)
35+
{
36+
builder.ConfigureAppConfiguration((context, config) =>
37+
{
38+
var overrides = new Dictionary<string, string>
39+
{
40+
["Auth0:Domain"] = "test",
41+
["Auth0:Audience"] = "test-audience",
42+
["AzureAD:ApiScope"] = "test-scope",
43+
["ConnectionStrings:PathfinderCS"] = "DataSource=:memory:"
44+
};
45+
46+
config.AddInMemoryCollection(overrides);
47+
});
48+
49+
builder.ConfigureServices(services =>
50+
{
51+
services.RemoveAll<DbContextOptions<PathfinderContext>>();
52+
services.RemoveAll<DbContextOptions>();
53+
services.RemoveAll<IDatabaseProvider>();
54+
services.RemoveAll<IDbContextOptionsConfiguration<PathfinderContext>>();
55+
56+
RemoveHostedService<MigrationService>(services);
57+
RemoveHostedService<AchievementSyncBackgroundService>(services);
58+
59+
services.AddDbContext<PathfinderContext>(options =>
60+
{
61+
options.UseSqlite(_connection);
62+
options.ReplaceService<IModelCustomizer, TestModelCustomizer>();
63+
options.AddInterceptors(new TimestampSaveChangesInterceptor());
64+
});
65+
66+
services.AddAuthentication(options =>
67+
{
68+
options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName;
69+
options.DefaultChallengeScheme = TestAuthHandler.SchemeName;
70+
})
71+
.AddScheme<TestAuthOptions, TestAuthHandler>(
72+
TestAuthHandler.SchemeName,
73+
options => { options.Permissions = _permissions; });
74+
});
75+
}
76+
77+
public async Task InitializeAsync()
78+
{
79+
using var scope = Services.CreateScope();
80+
var dbContext = scope.ServiceProvider.GetRequiredService<PathfinderContext>();
81+
await dbContext.Database.EnsureCreatedAsync();
82+
}
83+
84+
protected override void Dispose(bool disposing)
85+
{
86+
if (disposing)
87+
{
88+
_connection.Dispose();
89+
}
90+
91+
base.Dispose(disposing);
92+
}
93+
94+
private static void RemoveHostedService<TService>(IServiceCollection services)
95+
where TService : class, IHostedService
96+
{
97+
var descriptors = services
98+
.Where(descriptor =>
99+
descriptor.ServiceType == typeof(IHostedService) &&
100+
descriptor.ImplementationType == typeof(TService))
101+
.ToList();
102+
103+
foreach (var descriptor in descriptors)
104+
{
105+
services.Remove(descriptor);
106+
}
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)