feat/Update_Controllers_to_Allow_for_Premium_Membership
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
using System.Security.Claims;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Xunit;
|
||||
|
||||
namespace JobTrackerApi.Tests;
|
||||
|
||||
public sealed class AiPrivacyPolicyTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false, true, true, "local")]
|
||||
[InlineData(true, false, true, "local")]
|
||||
[InlineData(true, true, false, "local")]
|
||||
[InlineData(true, true, true, "gemini")]
|
||||
public async Task External_processing_requires_admin_gate_user_consent_and_pro(
|
||||
bool adminEnabled,
|
||||
bool userAllowed,
|
||||
bool pro,
|
||||
string expectedProvider)
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync(adminEnabled);
|
||||
await fixture.CreateUserAsync(userAllowed, pro);
|
||||
|
||||
var decision = await fixture.Policy.EvaluateAsync("user-1");
|
||||
|
||||
Assert.Equal(expectedProvider != "local", decision.ExternalProcessingAllowed);
|
||||
Assert.Equal(expectedProvider, decision.Provider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Disabled_ai_always_forces_local_processing()
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync(adminEnabled: true);
|
||||
await fixture.CreateUserAsync(externalAllowed: true, pro: true, aiEnabled: false);
|
||||
|
||||
var decision = await fixture.Policy.EvaluateAsync("user-1");
|
||||
|
||||
Assert.False(decision.ExternalProcessingAllowed);
|
||||
Assert.Equal("local", decision.Provider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cv_request_carries_external_permission_only_after_the_policy_allows_it()
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync(adminEnabled: true);
|
||||
await fixture.CreateUserAsync(externalAllowed: true, pro: true);
|
||||
var context = new DefaultHttpContext
|
||||
{
|
||||
User = new ClaimsPrincipal(new ClaimsIdentity(
|
||||
new[] { new Claim(ClaimTypes.NameIdentifier, "user-1") }, "local")),
|
||||
};
|
||||
var capture = new CaptureHandler();
|
||||
var handler = new AiPrivacyHeaderHandler(new HttpContextAccessor { HttpContext = context }, fixture.Policy)
|
||||
{
|
||||
InnerHandler = capture,
|
||||
};
|
||||
|
||||
using var client = new HttpClient(handler);
|
||||
await client.PostAsync("http://ai-service/cv/rewrite", new StringContent("{}"));
|
||||
|
||||
Assert.Equal("true", capture.ExternalAllowed);
|
||||
}
|
||||
|
||||
private sealed class CaptureHandler : HttpMessageHandler
|
||||
{
|
||||
public string? ExternalAllowed { get; private set; }
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
ExternalAllowed = request.Headers.TryGetValues(AiPrivacyPolicy.ExternalAllowedHeader, out var values)
|
||||
? values.Single()
|
||||
: null;
|
||||
return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Fixture : IAsyncDisposable
|
||||
{
|
||||
private readonly SqliteConnection _connection;
|
||||
private readonly ServiceProvider _services;
|
||||
public AiPrivacyPolicy Policy { get; }
|
||||
|
||||
private Fixture(SqliteConnection connection, ServiceProvider services, AiPrivacyPolicy policy)
|
||||
{
|
||||
_connection = connection;
|
||||
_services = services;
|
||||
Policy = policy;
|
||||
}
|
||||
|
||||
public static async Task<Fixture> CreateAsync(bool adminEnabled)
|
||||
{
|
||||
var connection = new SqliteConnection("Data Source=:memory:");
|
||||
await connection.OpenAsync();
|
||||
var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Ai:ExternalProcessingEnabled"] = adminEnabled.ToString(),
|
||||
["Ai:ExternalProvider"] = "gemini",
|
||||
}).Build();
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddScoped<CurrentUserService>();
|
||||
services.AddScoped<ICurrentUserService>(provider => provider.GetRequiredService<CurrentUserService>());
|
||||
services.AddDbContext<JobTrackerContext>((_, options) => options.UseSqlite(connection));
|
||||
services.AddIdentityCore<ApplicationUser>().AddRoles<IdentityRole>().AddEntityFrameworkStores<JobTrackerContext>();
|
||||
var provider = services.BuildServiceProvider();
|
||||
await using var scope = provider.CreateAsyncScope();
|
||||
await scope.ServiceProvider.GetRequiredService<JobTrackerContext>().Database.EnsureCreatedAsync();
|
||||
return new Fixture(connection, provider, new AiPrivacyPolicy(configuration, provider.GetRequiredService<IServiceScopeFactory>()));
|
||||
}
|
||||
|
||||
public async Task CreateUserAsync(bool externalAllowed, bool pro, bool aiEnabled = true)
|
||||
{
|
||||
await using var scope = _services.CreateAsyncScope();
|
||||
var roles = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
|
||||
if (pro) await roles.CreateAsync(new IdentityRole("Premium"));
|
||||
var users = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
Id = "user-1",
|
||||
UserName = "synthetic@example.test",
|
||||
Email = "synthetic@example.test",
|
||||
AiEnabled = aiEnabled,
|
||||
ExternalAiProcessingAllowed = externalAllowed,
|
||||
};
|
||||
Assert.True((await users.CreateAsync(user)).Succeeded);
|
||||
if (pro) Assert.True((await users.AddToRoleAsync(user, "Premium")).Succeeded);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _services.DisposeAsync();
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user