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

View File

@@ -28,6 +28,7 @@ using ExchangeSharp;
using LNURL; using LNURL;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Playwright; using Microsoft.Playwright;
using static Microsoft.Playwright.Assertions; using static Microsoft.Playwright.Assertions;
using NBitcoin; using NBitcoin;
@@ -200,7 +201,8 @@ namespace BTCPayServer.Tests
await s.Page.FillAsync("#Email", changedEmail); await s.Page.FillAsync("#Email", changedEmail);
await s.ClickPagePrimary(); await s.ClickPagePrimary();
await s.FindAlertMessage(); 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.FindByNameAsync(changedEmail));
Assert.NotNull(await manager.FindByEmailAsync(changedEmail)); Assert.NotNull(await manager.FindByEmailAsync(changedEmail));
} }

View File

@@ -26,6 +26,7 @@ using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin; using NBitcoin;
using NBitcoin.DataEncoders; using NBitcoin.DataEncoders;
using NBitcoin.Payment; using NBitcoin.Payment;
@@ -58,7 +59,8 @@ namespace BTCPayServer.Tests
public async Task MakeAdmin(bool isAdmin = true) 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); var u = await userManager.FindByIdAsync(UserId);
if (isAdmin) if (isAdmin)
await userManager.AddToRoleAsync(u, Roles.ServerAdmin); await userManager.AddToRoleAsync(u, Roles.ServerAdmin);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,6 +26,10 @@ public class BTCPayServerSecurityStampValidator(
ConcurrentDictionary<string, DateTimeOffset> _DisabledUsers = new ConcurrentDictionary<string, DateTimeOffset>(); ConcurrentDictionary<string, DateTimeOffset> _DisabledUsers = new ConcurrentDictionary<string, DateTimeOffset>();
public bool HasAny => !_DisabledUsers.IsEmpty; 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) public void Add(string user)
{ {
_DisabledUsers.TryAdd(user, DateTimeOffset.UtcNow); _DisabledUsers.TryAdd(user, DateTimeOffset.UtcNow);

View File

@@ -6,6 +6,7 @@ using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Data; using BTCPayServer.Data;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Services.Notifications namespace BTCPayServer.Services.Notifications
{ {
@@ -13,25 +14,15 @@ namespace BTCPayServer.Services.Notifications
{ {
public string UserId { get; set; } 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) public async Task SendNotification(INotificationScope scope, BaseNotification notification)
{ {
ArgumentNullException.ThrowIfNull(scope); ArgumentNullException.ThrowIfNull(scope);
ArgumentNullException.ThrowIfNull(notification); ArgumentNullException.ThrowIfNull(notification);
var users = await GetUsers(scope, notification.Identifier); var users = await GetUsers(scope, notification.Identifier);
await using (var db = _contextFactory.CreateContext()) await using (var db = contextFactory.CreateContext())
{ {
foreach (var uid in users) foreach (var uid in users)
{ {
@@ -48,7 +39,7 @@ namespace BTCPayServer.Services.Notifications
} }
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }
_notificationManager.InvalidateNotificationCache(users); notificationManager.InvalidateNotificationCache(users);
} }
public BaseNotification GetBaseNotification(NotificationData notificationData) public BaseNotification GetBaseNotification(NotificationData notificationData)
@@ -58,7 +49,7 @@ namespace BTCPayServer.Services.Notifications
private async Task<string[]> GetUsers(INotificationScope scope, string notificationIdentifier) 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 split = notificationIdentifier.Split('_', StringSplitOptions.None);
var terms = new List<string>(); var terms = new List<string>();
@@ -71,8 +62,8 @@ namespace BTCPayServer.Services.Notifications
{ {
case AdminScope _: 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; break;
} }
case StoreScope s: case StoreScope s: