Validate scopes of service injections in tests

This commit is contained in:
Nicolas Dorier
2025-11-14 23:44:03 +09:00
parent 62552a7bfe
commit 749c772218
11 changed files with 50 additions and 66 deletions

View File

@@ -169,6 +169,10 @@ namespace BTCPayServer.Tests
#endif
var conf = confBuilder.Build();
_Host = new WebHostBuilder()
.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true;
})
.UseConfiguration(conf)
.UseContentRoot(FindBTCPayServerDirectory())
.UseWebRoot(Path.Combine(FindBTCPayServerDirectory(), "wwwroot"))
@@ -284,10 +288,7 @@ namespace BTCPayServer.Tests
public string IntegratedLightning { get; internal set; }
public bool InContainer { get; internal set; }
public T GetService<T>()
{
return _Host.Services.GetRequiredService<T>();
}
public T GetService<T>() => _Host.Services.GetRequiredService<T>();
public IServiceProvider ServiceProvider => _Host.Services;

View File

@@ -28,6 +28,7 @@ using ExchangeSharp;
using LNURL;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Playwright;
using static Microsoft.Playwright.Assertions;
using NBitcoin;
@@ -200,7 +201,8 @@ namespace BTCPayServer.Tests
await s.Page.FillAsync("#Email", changedEmail);
await s.ClickPagePrimary();
await s.FindAlertMessage();
var manager = tester.PayTester.GetService<UserManager<ApplicationUser>>();
using var scope = tester.PayTester.ServiceProvider.CreateScope();
var manager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
Assert.NotNull(await manager.FindByNameAsync(changedEmail));
Assert.NotNull(await manager.FindByEmailAsync(changedEmail));
}

View File

@@ -26,6 +26,7 @@ using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
@@ -58,7 +59,8 @@ namespace BTCPayServer.Tests
public async Task MakeAdmin(bool isAdmin = true)
{
var userManager = parent.PayTester.GetService<UserManager<ApplicationUser>>();
using var scope = parent.PayTester.ServiceProvider.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var u = await userManager.FindByIdAsync(UserId);
if (isAdmin)
await userManager.AddToRoleAsync(u, Roles.ServerAdmin);

View File

@@ -15,7 +15,6 @@ using BTCPayServer.Data;
using BTCPayServer.Plugins.Webhooks.Controllers;
using BTCPayServer.Security;
using BTCPayServer.Security.Greenfield;
using BTCPayServer.Plugins.Emails.Services;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -38,26 +37,13 @@ using WebhookDeliveryData = BTCPayServer.Client.Models.WebhookDeliveryData;
namespace BTCPayServer.Controllers.Greenfield
{
public class BTCPayServerClientFactory : IBTCPayServerClientFactory
{
private readonly StoreRepository _storeRepository;
private readonly IOptionsMonitor<IdentityOptions> _identityOptions;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IServiceProvider _serviceProvider;
public BTCPayServerClientFactory(
public class BTCPayServerClientFactory(
StoreRepository storeRepository,
IOptionsMonitor<IdentityOptions> identityOptions,
UserManager<ApplicationUser> userManager,
IServiceProvider serviceProvider)
IServiceProvider serviceProvider,
IServiceScopeFactory servicesScopeFactory)
: IBTCPayServerClientFactory
{
_storeRepository = storeRepository;
_identityOptions = identityOptions;
_userManager = userManager;
_serviceProvider = serviceProvider;
}
public Task<BTCPayServerClient> Create(string userId, params string[] storeIds)
{
return Create(userId, storeIds, new DefaultHttpContext()
@@ -74,17 +60,19 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<BTCPayServerClient> Create(string userId, string[] storeIds, HttpContext context)
{
using var scope = servicesScopeFactory.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
if (!string.IsNullOrEmpty(userId))
{
var user = await _userManager.FindByIdAsync(userId);
var user = await userManager.FindByIdAsync(userId);
List<Claim> claims = new List<Claim>
{
new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, userId),
new Claim(identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, userId),
new Claim(GreenfieldConstants.ClaimTypes.Permission,
Permission.Create(Policies.Unrestricted).ToString())
};
claims.AddRange((await _userManager.GetRolesAsync(user)).Select(s =>
new Claim(_identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, s)));
claims.AddRange((await userManager.GetRolesAsync(user)).Select(s =>
new Claim(identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, s)));
context.User =
new ClaimsPrincipal(new ClaimsIdentity(claims,
$"Local{GreenfieldConstants.AuthenticationType}WithUser"));
@@ -95,22 +83,22 @@ namespace BTCPayServer.Controllers.Greenfield
new ClaimsPrincipal(new ClaimsIdentity(
new List<Claim>()
{
new(_identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, Roles.ServerAdmin)
new(identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, Roles.ServerAdmin)
},
$"Local{GreenfieldConstants.AuthenticationType}"));
}
if (storeIds?.Any() is true)
{
context.SetStoreData(await _storeRepository.FindStore(storeIds.First()));
context.SetStoresData(await _storeRepository.GetStoresByUserId(userId, storeIds));
context.SetStoreData(await storeRepository.FindStore(storeIds.First()));
context.SetStoresData(await storeRepository.GetStoresByUserId(userId, storeIds));
}
else
{
context.SetStoresData(await _storeRepository.GetStoresByUserId(userId));
context.SetStoresData(await storeRepository.GetStoresByUserId(userId));
}
return ActivatorUtilities.CreateInstance<LocalBTCPayServerClient>(_serviceProvider,
return ActivatorUtilities.CreateInstance<LocalBTCPayServerClient>(serviceProvider,
new LocalHttpContextAccessor() { HttpContext = context });
}

View File

@@ -39,7 +39,6 @@ namespace BTCPayServer.Controllers
public partial class UIInvoiceController : Controller
{
readonly InvoiceRepository _InvoiceRepository;
private readonly WalletRepository _walletRepository;
readonly RateFetcher _RateProvider;
readonly StoreRepository _StoreRepository;
readonly UserManager<ApplicationUser> _UserManager;
@@ -71,14 +70,12 @@ namespace BTCPayServer.Controllers
public UIInvoiceController(
InvoiceRepository invoiceRepository,
WalletRepository walletRepository,
DisplayFormatter displayFormatter,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
RateFetcher rateProvider,
StoreRepository storeRepository,
EventAggregator eventAggregator,
ContentSecurityPolicies csp,
BTCPayNetworkProvider networkProvider,
PayoutMethodHandlerDictionary payoutHandlers,
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
@@ -105,7 +102,6 @@ namespace BTCPayServer.Controllers
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_walletRepository = walletRepository;
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_UserManager = userManager;
_EventAggregator = eventAggregator;

View File

@@ -43,15 +43,13 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public const string LightningLikePayoutHandlerClearnetNamedClient =
nameof(LightningLikePayoutHandlerClearnetNamedClient);
private readonly IHttpClientFactory _httpClientFactory;
private readonly UserService _userService;
private readonly IAuthorizationService _authorizationService;
public LightningLikePayoutHandler(
PayoutMethodId payoutMethodId,
IOptions<LightningNetworkOptions> options,
BTCPayNetwork network,
PaymentMethodHandlerDictionary paymentHandlers,
IHttpClientFactory httpClientFactory, UserService userService, IAuthorizationService authorizationService)
IHttpClientFactory httpClientFactory)
{
_paymentHandlers = paymentHandlers;
Network = network;
@@ -59,8 +57,6 @@ namespace BTCPayServer.Data.Payouts.LightningLike
_options = options;
PaymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
_httpClientFactory = httpClientFactory;
_userService = userService;
_authorizationService = authorizationService;
Currency = network.CryptoCode;
}

View File

@@ -427,7 +427,7 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
services.AddSingleton<PayoutMethodHandlerDictionary>();
services.AddSingleton<NotificationManager>();
services.AddScoped<NotificationSender>();
services.AddSingleton<NotificationSender>();
RegisterExchangeRecommendations(services);
services.AddSingleton<DefaultRulesCollection>();

View File

@@ -11,6 +11,7 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
using QRCoder;
@@ -18,14 +19,12 @@ namespace BTCPayServer.Plugins.Emails.HostedServices;
public class UserEventHostedService(
EventAggregator eventAggregator,
UserManager<ApplicationUser> userManager,
IServiceScopeFactory serviceScopeFactory,
ISettingsAccessor<ServerSettings> serverSettings,
NotificationSender notificationSender,
Logs logs)
: EventHostedServiceBase(eventAggregator, logs)
{
public UserManager<ApplicationUser> UserManager { get; } = userManager;
protected override void SubscribeToEvents()
{
SubscribeAny<UserEvent>();
@@ -117,7 +116,9 @@ public class UserEventHostedService(
private async Task<TriggerEvent> CreateTriggerEvent(string trigger, JObject model, ApplicationUser user)
{
var admins = await UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
using var scope = serviceScopeFactory.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var admins = await userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
var adminMailboxes = string.Join(", ", admins.Select(a => a.GetMailboxAddress().ToString()).ToArray());
model["Admins"] = new JObject()
{

View File

@@ -18,6 +18,7 @@ using BTCPayServer.Services.Rates;
using Dapper;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using static BTCPayServer.Data.Subscriptions.SubscriberData;
@@ -30,7 +31,7 @@ public class SubscriptionHostedService(
EventAggregator eventAggregator,
ApplicationDbContextFactory applicationDbContextFactory,
SettingsRepository settingsRepository,
UIInvoiceController invoiceController,
IServiceScopeFactory scopeFactory,
CurrencyNameTable currencyNameTable,
LinkGenerator linkGenerator,
Logs logger) : EventHostedServiceBase(eventAggregator, logger), IPeriodicTask
@@ -162,6 +163,8 @@ public class SubscriptionHostedService(
if (amount > 0)
{
using var scope = scopeFactory.CreateScope();
var invoiceController = scope.ServiceProvider.GetRequiredService<UIInvoiceController>();
var request = await invoiceController.CreateInvoiceCoreRaw(new()
{
Currency = plan.Currency,

View File

@@ -26,6 +26,10 @@ public class BTCPayServerSecurityStampValidator(
ConcurrentDictionary<string, DateTimeOffset> _DisabledUsers = new ConcurrentDictionary<string, DateTimeOffset>();
public bool HasAny => !_DisabledUsers.IsEmpty;
/// <summary>
/// Note that you also need to invalidate the security stamp of the user
/// </summary>
/// <param name="user"></param>
public void Add(string user)
{
_DisabledUsers.TryAdd(user, DateTimeOffset.UtcNow);

View File

@@ -6,6 +6,7 @@ using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Services.Notifications
{
@@ -13,25 +14,15 @@ namespace BTCPayServer.Services.Notifications
{
public string UserId { get; set; }
}
public class NotificationSender
public class NotificationSender(ApplicationDbContextFactory contextFactory, IServiceScopeFactory scopeFactory, NotificationManager notificationManager)
{
private readonly ApplicationDbContextFactory _contextFactory;
private readonly UserManager<ApplicationUser> _userManager;
private readonly NotificationManager _notificationManager;
public NotificationSender(ApplicationDbContextFactory contextFactory, UserManager<ApplicationUser> userManager, NotificationManager notificationManager)
{
_contextFactory = contextFactory;
_userManager = userManager;
_notificationManager = notificationManager;
}
public async Task SendNotification(INotificationScope scope, BaseNotification notification)
{
ArgumentNullException.ThrowIfNull(scope);
ArgumentNullException.ThrowIfNull(notification);
var users = await GetUsers(scope, notification.Identifier);
await using (var db = _contextFactory.CreateContext())
await using (var db = contextFactory.CreateContext())
{
foreach (var uid in users)
{
@@ -48,7 +39,7 @@ namespace BTCPayServer.Services.Notifications
}
await db.SaveChangesAsync();
}
_notificationManager.InvalidateNotificationCache(users);
notificationManager.InvalidateNotificationCache(users);
}
public BaseNotification GetBaseNotification(NotificationData notificationData)
@@ -58,7 +49,7 @@ namespace BTCPayServer.Services.Notifications
private async Task<string[]> GetUsers(INotificationScope scope, string notificationIdentifier)
{
await using var ctx = _contextFactory.CreateContext();
await using var ctx = contextFactory.CreateContext();
var split = notificationIdentifier.Split('_', StringSplitOptions.None);
var terms = new List<string>();
@@ -71,8 +62,8 @@ namespace BTCPayServer.Services.Notifications
{
case AdminScope _:
{
query = _userManager.GetUsersInRoleAsync(Roles.ServerAdmin).Result.AsQueryable();
using var s = scopeFactory.CreateScope();
query = s.ServiceProvider.GetService<UserManager<ApplicationUser>>().GetUsersInRoleAsync(Roles.ServerAdmin).Result.AsQueryable();
break;
}
case StoreScope s: