mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-25 01:57:02 +01:00
Merge pull request #16906 from JPVenson/fix/UserManagerCollation
Fix/user manager collation
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -277,3 +277,7 @@ apiclient/generated
|
||||
|
||||
# Omnisharp crash logs
|
||||
mono_crash.*.json
|
||||
|
||||
# Devcontainer temp files
|
||||
.devcontainer/devcontainer-lock.json
|
||||
dotnet/
|
||||
|
||||
@@ -142,13 +142,15 @@ public class StartupController : BaseJellyfinApiController
|
||||
return BadRequest("Password must not be empty");
|
||||
}
|
||||
|
||||
if (startupUserDto.Name is not null)
|
||||
{
|
||||
user.Username = startupUserDto.Name;
|
||||
}
|
||||
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
|
||||
#pragma warning disable CA1309 // Use ordinal string comparison
|
||||
if (startupUserDto.Name is not null && !startupUserDto.Name.Equals(user.Username, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
await _userManager.RenameUser(user.Id, user.Username, startupUserDto.Name).ConfigureAwait(false);
|
||||
}
|
||||
#pragma warning restore CA1309 // Use ordinal string comparison
|
||||
|
||||
if (!string.IsNullOrEmpty(startupUserDto.Password))
|
||||
{
|
||||
await _userManager.ChangePassword(user.Id, startupUserDto.Password).ConfigureAwait(false);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#pragma warning disable CA1307
|
||||
#pragma warning disable RS0030 // Do not use banned APIs
|
||||
|
||||
using System;
|
||||
@@ -162,12 +161,8 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
|
||||
using var dbContext = _dbProvider.CreateDbContext();
|
||||
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
||||
#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
|
||||
#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
|
||||
return UserQuery(dbContext)
|
||||
.FirstOrDefault(u => u.Username.ToUpper() == name.ToUpper());
|
||||
#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
|
||||
#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
|
||||
.FirstOrDefault(u => u.NormalizedUsername == name.ToUpperInvariant());
|
||||
#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
||||
}
|
||||
|
||||
@@ -188,10 +183,8 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
||||
#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
|
||||
#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
|
||||
if (await dbContext.Users
|
||||
.AnyAsync(u => u.Username.ToUpper() == newName.ToUpper() && u.Id != userId)
|
||||
.AnyAsync(u => u.NormalizedUsername == newName.ToUpperInvariant() && u.Id != userId)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
throw new ArgumentException(string.Format(
|
||||
@@ -199,8 +192,6 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
"A user with the name '{0}' already exists.",
|
||||
newName));
|
||||
}
|
||||
#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
|
||||
#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
|
||||
#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
||||
|
||||
user = await UserQuery(dbContext)
|
||||
@@ -210,6 +201,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
?? throw new ResourceNotFoundException(nameof(userId));
|
||||
|
||||
user.Username = newName;
|
||||
user.NormalizedUsername = newName.ToUpperInvariant();
|
||||
await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -259,10 +251,8 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
||||
#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
|
||||
#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
|
||||
if (await dbContext.Users
|
||||
.AnyAsync(u => u.Username.ToUpper() == name.ToUpper())
|
||||
.AnyAsync(u => u.NormalizedUsername == name.ToUpperInvariant())
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
throw new ArgumentException(string.Format(
|
||||
@@ -270,8 +260,6 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
"A user with the name '{0}' already exists.",
|
||||
name));
|
||||
}
|
||||
#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
|
||||
#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
|
||||
#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
||||
|
||||
newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
/// <summary>
|
||||
/// Part 2 Migration for NormalisedUsername.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2026-05-22T09:23:04", nameof(UpdateNormalizedUsername), Stage = Stages.JellyfinMigrationStageTypes.CoreInitialisation)]
|
||||
#pragma warning disable SA1649 // File name should match first type name
|
||||
public class UpdateNormalizedUsername : IAsyncMigrationRoutine
|
||||
#pragma warning restore SA1649 // File name should match first type name
|
||||
{
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _contextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UpdateNormalizedUsername"/> class.
|
||||
/// </summary>
|
||||
/// <param name="contextFactory">Db Context factory.</param>
|
||||
public UpdateNormalizedUsername(IDbContextFactory<JellyfinDbContext> contextFactory)
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task PerformAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var dbContext = await _contextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var users = await dbContext.Users.ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
foreach (var user in users)
|
||||
{
|
||||
user.NormalizedUsername = user.Username.ToUpperInvariant();
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ namespace Jellyfin.Database.Implementations.Entities
|
||||
ArgumentException.ThrowIfNullOrEmpty(passwordResetProviderId);
|
||||
|
||||
Username = username;
|
||||
NormalizedUsername = username.ToUpperInvariant();
|
||||
AuthenticationProviderId = authenticationProviderId;
|
||||
PasswordResetProviderId = passwordResetProviderId;
|
||||
|
||||
@@ -73,6 +74,16 @@ namespace Jellyfin.Database.Implementations.Entities
|
||||
[StringLength(255)]
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user's normalized name.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Required, Max length = 255.
|
||||
/// </remarks>
|
||||
[MaxLength(255)]
|
||||
[StringLength(255)]
|
||||
public string NormalizedUsername { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user's password, or <c>null</c> if none is set.
|
||||
/// </summary>
|
||||
|
||||
@@ -50,6 +50,10 @@ namespace Jellyfin.Database.Implementations.ModelConfiguration
|
||||
builder
|
||||
.HasIndex(entity => entity.Username)
|
||||
.IsUnique();
|
||||
|
||||
builder
|
||||
.HasIndex(entity => entity.NormalizedUsername)
|
||||
.IsUnique();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddNormalizedUsername : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// this is the first part of the migration. Add the column.
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "NormalizedUsername",
|
||||
table: "Users",
|
||||
type: "TEXT",
|
||||
maxLength: 255,
|
||||
nullable: false,
|
||||
defaultValue: string.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "NormalizedUsername",
|
||||
table: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddUniqueNormalizedUsernameIndex : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Users_NormalizedUsername",
|
||||
table: "Users",
|
||||
column: "NormalizedUsername",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Users_NormalizedUsername",
|
||||
table: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.11");
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
|
||||
{
|
||||
@@ -1303,6 +1303,11 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<bool>("MustUpdatePassword")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("NormalizedUsername")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasMaxLength(65535)
|
||||
.HasColumnType("TEXT");
|
||||
@@ -1345,6 +1350,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedUsername")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Locking;
|
||||
using Jellyfin.Database.Providers.Sqlite;
|
||||
using Jellyfin.Server.Implementations.Users;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Events;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Tests.Users
|
||||
{
|
||||
public sealed class UserManagerNormalizedUsernameTests : IDisposable
|
||||
{
|
||||
private readonly SqliteConnection _connection;
|
||||
private readonly DbContextOptions<JellyfinDbContext> _dbOptions;
|
||||
private readonly UserManager _userManager;
|
||||
|
||||
public UserManagerNormalizedUsernameTests()
|
||||
{
|
||||
_connection = new SqliteConnection("Data Source=:memory:");
|
||||
_connection.Open();
|
||||
|
||||
_dbOptions = new DbContextOptionsBuilder<JellyfinDbContext>()
|
||||
.UseSqlite(_connection)
|
||||
.Options;
|
||||
|
||||
// Create the schema
|
||||
using var ctx = CreateDbContext();
|
||||
ctx.Database.EnsureCreated();
|
||||
|
||||
var factory = new Mock<IDbContextFactory<JellyfinDbContext>>();
|
||||
factory.Setup(f => f.CreateDbContext()).Returns(CreateDbContext);
|
||||
factory.Setup(f => f.CreateDbContextAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(CreateDbContext);
|
||||
|
||||
var cryptoProvider = new Mock<ICryptoProvider>();
|
||||
var configManager = new Mock<IServerConfigurationManager>();
|
||||
var appPaths = new Mock<IServerApplicationPaths>();
|
||||
appPaths.Setup(x => x.ProgramDataPath).Returns(Path.GetTempPath());
|
||||
configManager.Setup(x => x.ApplicationPaths).Returns(appPaths.Object);
|
||||
|
||||
var appHost = new Mock<IApplicationHost>();
|
||||
|
||||
var defaultAuthProvider = new DefaultAuthenticationProvider(
|
||||
NullLogger<DefaultAuthenticationProvider>.Instance,
|
||||
cryptoProvider.Object);
|
||||
var invalidAuthProvider = new InvalidAuthProvider();
|
||||
var defaultPasswordResetProvider = new DefaultPasswordResetProvider(
|
||||
configManager.Object,
|
||||
appHost.Object);
|
||||
|
||||
_userManager = new UserManager(
|
||||
factory.Object,
|
||||
new NoopEventManager(),
|
||||
new Mock<INetworkManager>().Object,
|
||||
appHost.Object,
|
||||
new Mock<IImageProcessor>().Object,
|
||||
NullLogger<UserManager>.Instance,
|
||||
configManager.Object,
|
||||
new IPasswordResetProvider[] { defaultPasswordResetProvider },
|
||||
new IAuthenticationProvider[] { defaultAuthProvider, invalidAuthProvider });
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_userManager.Dispose();
|
||||
_connection.Dispose();
|
||||
}
|
||||
|
||||
private JellyfinDbContext CreateDbContext()
|
||||
{
|
||||
return new JellyfinDbContext(
|
||||
_dbOptions,
|
||||
NullLogger<JellyfinDbContext>.Instance,
|
||||
new SqliteDatabaseProvider(null!, NullLogger<SqliteDatabaseProvider>.Instance),
|
||||
new NoLockBehavior(NullLogger<NoLockBehavior>.Instance));
|
||||
}
|
||||
|
||||
// ----- GetUserByName tests -----
|
||||
|
||||
[Theory]
|
||||
// German umlauts
|
||||
[InlineData("münchen", "MÜNCHEN")]
|
||||
// Spanish tilde-n
|
||||
[InlineData("Ñoño", "ÑOÑO")]
|
||||
// ASCII, invariant uppercase lookup
|
||||
[InlineData("jellyfin", "JELLYFIN")]
|
||||
// Turkish cedilla: invariant 'i' uppercases to 'I' (U+0049), not Turkish 'İ' (U+0130)
|
||||
[InlineData("Çelebi", "ÇELEBI")]
|
||||
public async Task GetUserByName_WithNonAsciiUsername_FindsUserByNormalizedName(
|
||||
string username, string normalizedLookup)
|
||||
{
|
||||
await _userManager.CreateUserAsync(username);
|
||||
|
||||
var found = _userManager.GetUserByName(normalizedLookup);
|
||||
|
||||
Assert.NotNull(found);
|
||||
Assert.Equal(username, found.Username);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// German umlaut, look up by both upper and lower case
|
||||
[InlineData("münchen")]
|
||||
// Spanish tilde-n
|
||||
[InlineData("Ñoño")]
|
||||
// lowercase 'i' — invariant ToUpperInvariant gives 'I', not Turkish 'İ'
|
||||
[InlineData("ali")]
|
||||
// mixed ASCII + umlaut
|
||||
[InlineData("testüser")]
|
||||
public async Task GetUserByName_WithVariousCase_FindsUserCaseInsensitively(string username)
|
||||
{
|
||||
await _userManager.CreateUserAsync(username);
|
||||
|
||||
var upperFound = _userManager.GetUserByName(username.ToUpperInvariant());
|
||||
var lowerFound = _userManager.GetUserByName(username.ToLowerInvariant());
|
||||
var exactFound = _userManager.GetUserByName(username);
|
||||
|
||||
Assert.NotNull(upperFound);
|
||||
Assert.NotNull(lowerFound);
|
||||
Assert.NotNull(exactFound);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("nonexistent")]
|
||||
// No user with NormalizedUsername = "MÜNCHEN" has been created
|
||||
[InlineData("MÜNCHEN")]
|
||||
public void GetUserByName_WhenUserDoesNotExist_ReturnsNull(string lookupName)
|
||||
{
|
||||
var result = _userManager.GetUserByName(lookupName);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// ----- CreateUserAsync duplicate detection tests -----
|
||||
|
||||
[Theory]
|
||||
// German umlaut, case-swapped duplicate
|
||||
[InlineData("münchen", "MÜNCHEN")]
|
||||
// Spanish tilde-n, lowercase duplicate
|
||||
[InlineData("Ñoño", "ñoño")]
|
||||
// ASCII, uppercase duplicate
|
||||
[InlineData("alice", "ALICE")]
|
||||
// Turkish cedilla: "çelebi".ToUpperInvariant() == "ÇELEBI" == "ÇELEBI".ToUpperInvariant()
|
||||
[InlineData("çelebi", "ÇELEBI")]
|
||||
public async Task CreateUserAsync_WhenNormalizedNameAlreadyExists_ThrowsArgumentException(
|
||||
string existingUsername, string duplicateUsername)
|
||||
{
|
||||
await _userManager.CreateUserAsync(existingUsername);
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => _userManager.CreateUserAsync(duplicateUsername));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// Different non-ASCII names that do not collide after normalization
|
||||
[InlineData("münchen", "münchen2")]
|
||||
[InlineData("ali", "ali2")]
|
||||
// Visually similar but different Unicode code points: ñ (U+00F1) vs n (U+006E)
|
||||
[InlineData("noño", "nono")]
|
||||
public async Task CreateUserAsync_WithDistinctNonAsciiUsernames_CreatesBothUsers(
|
||||
string firstUsername, string secondUsername)
|
||||
{
|
||||
var first = await _userManager.CreateUserAsync(firstUsername);
|
||||
var second = await _userManager.CreateUserAsync(secondUsername);
|
||||
|
||||
Assert.NotNull(first);
|
||||
Assert.NotNull(second);
|
||||
Assert.NotEqual(first.Id, second.Id);
|
||||
}
|
||||
|
||||
// ----- RenameUser tests -----
|
||||
|
||||
[Theory]
|
||||
// Rename to non-ASCII name
|
||||
[InlineData("alice", "münchen")]
|
||||
// Rename between similar non-ASCII and ASCII
|
||||
[InlineData("müller", "mueller")]
|
||||
// Contains 'i': invariant uppercase is always 'I', never Turkish 'İ'
|
||||
[InlineData("ali", "ALI2")]
|
||||
// Rename to Spanish tilde-n name
|
||||
[InlineData("testuser", "Ñoño")]
|
||||
public async Task RenameUser_SetsNormalizedUsernameToUpperInvariant(
|
||||
string originalName, string newName)
|
||||
{
|
||||
var user = await _userManager.CreateUserAsync(originalName);
|
||||
|
||||
await _userManager.RenameUser(user.Id, originalName, newName);
|
||||
|
||||
var renamed = _userManager.GetUserById(user.Id);
|
||||
Assert.NotNull(renamed);
|
||||
Assert.Equal(newName, renamed.Username);
|
||||
Assert.Equal(newName.ToUpperInvariant(), renamed.NormalizedUsername);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// Same name different case: NormalizedUsername already taken
|
||||
[InlineData("münchen", "MÜNCHEN")]
|
||||
// Spanish, lowercase conflicts with existing uppercase-normalised entry
|
||||
[InlineData("Ñoño", "ñoño")]
|
||||
// ASCII, capitalised conflict
|
||||
[InlineData("alice", "Alice")]
|
||||
// Mixed ASCII + umlaut
|
||||
[InlineData("testüser", "TESTÜSER")]
|
||||
public async Task RenameUser_WhenNormalizedNameConflictsWithExistingUser_ThrowsArgumentException(
|
||||
string existingUsername, string conflictingNewName)
|
||||
{
|
||||
var targetUser = await _userManager.CreateUserAsync("renametarget");
|
||||
await _userManager.CreateUserAsync(existingUsername);
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => _userManager.RenameUser(targetUser.Id, "renametarget", conflictingNewName));
|
||||
}
|
||||
|
||||
private sealed class NoopEventManager : IEventManager
|
||||
{
|
||||
public void Publish<T>(T eventArgs)
|
||||
where T : EventArgs
|
||||
{
|
||||
}
|
||||
|
||||
public Task PublishAsync<T>(T eventArgs)
|
||||
where T : EventArgs
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user