Дочистить код-стайл: var встроенных типов, приватные доки, доки интерфейсам

Гейт csharp_style_var_for_built_in_types=false:warning (ломает сборку), остаток
выправлен dotnet format IDE0008 по 5 sln (только встроенные типы). Понижено 12
новых XML-доков private/internal, добавлены <summary> членам IContainerRules и
ITenantContext, 3 англоязычных комментария переведены на русский.
This commit is contained in:
Rustam Khalimov
2026-09-11 19:00:35 +03:00
parent dde86fd63c
commit 39c9bdc1b7
51 changed files with 3204 additions and 3215 deletions
+2 -1
View File
@@ -30,7 +30,8 @@ dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning dotnet_style_qualification_for_event = false:warning
# Члены # Члены
csharp_style_var_for_built_in_types = false:silent # var — запрещён для встроенных типов (ломает сборку), для очевидных/прочих — silent (§4 код-стайла)
csharp_style_var_for_built_in_types = false:warning
csharp_style_var_when_type_is_apparent = false:silent csharp_style_var_when_type_is_apparent = false:silent
csharp_style_var_elsewhere = false:silent csharp_style_var_elsewhere = false:silent
+3 -3
View File
@@ -111,8 +111,8 @@ public static class AuthEndpoints
HttpContext context, HttpContext context,
CancellationToken ct) CancellationToken ct)
{ {
var cookieName = cookieOptions.Value.Name; string cookieName = cookieOptions.Value.Name;
var rawToken = context.Request.Cookies[cookieName]; string? rawToken = context.Request.Cookies[cookieName];
// Пользователь разрешённой сессии — до её удаления (SessionMiddleware наполнил Items на старте запроса). // Пользователь разрешённой сессии — до её удаления (SessionMiddleware наполнил Items на старте запроса).
CurrentUser? user = context.GetCurrentUser(); CurrentUser? user = context.GetCurrentUser();
var logout = await authService.LogoutAsync(rawToken, ct); var logout = await authService.LogoutAsync(rawToken, ct);
@@ -166,7 +166,7 @@ public static class AuthEndpoints
var result = await authService.ChangePasswordAsync(user.Login, body.OldPassword, body.NewPassword, ct); var result = await authService.ChangePasswordAsync(user.Login, body.OldPassword, body.NewPassword, ct);
if (!result.Ok || result.NewToken is null) if (!result.Ok || result.NewToken is null)
{ {
var detail = result.Error == ChangePasswordResultDto.ErrorTooShort string detail = result.Error == ChangePasswordResultDto.ErrorTooShort
? PasswordTooShortDetail ? PasswordTooShortDetail
: WrongOldPasswordDetail; : WrongOldPasswordDetail;
return EndpointResults.BadRequest(detail); return EndpointResults.BadRequest(detail);
@@ -91,8 +91,8 @@ public static class OperatorAuthEndpoints
HttpContext context, HttpContext context,
CancellationToken ct) CancellationToken ct)
{ {
var cookieName = cookieOptions.Value.Name; string cookieName = cookieOptions.Value.Name;
var rawToken = context.Request.Cookies[cookieName]; string? rawToken = context.Request.Cookies[cookieName];
// Оператор разрешённой сессии — до её удаления (OperatorSessionMiddleware наполнил Items). // Оператор разрешённой сессии — до её удаления (OperatorSessionMiddleware наполнил Items).
CurrentOperator? operatorIdentity = context.GetCurrentOperator(); CurrentOperator? operatorIdentity = context.GetCurrentOperator();
await operatorAuthService.LogoutAsync(rawToken, ct); await operatorAuthService.LogoutAsync(rawToken, ct);
@@ -17,8 +17,8 @@ public sealed class OperatorBootstrapHostedService(
await using var scope = scopeFactory.CreateAsyncScope(); await using var scope = scopeFactory.CreateAsyncScope();
var operatorBootstrapService = scope.ServiceProvider.GetRequiredService<OperatorBootstrapService>(); var operatorBootstrapService = scope.ServiceProvider.GetRequiredService<OperatorBootstrapService>();
var login = configuration[OperatorBootstrapService.LoginEnvKey]; string? login = configuration[OperatorBootstrapService.LoginEnvKey];
var password = configuration[OperatorBootstrapService.PasswordEnvKey]; string? password = configuration[OperatorBootstrapService.PasswordEnvKey];
bool hasLogin = !string.IsNullOrWhiteSpace(login); bool hasLogin = !string.IsNullOrWhiteSpace(login);
bool hasPassword = !string.IsNullOrWhiteSpace(password); bool hasPassword = !string.IsNullOrWhiteSpace(password);
bool allowDevelopmentDefaults = environment.IsDevelopment(); bool allowDevelopmentDefaults = environment.IsDevelopment();
@@ -47,7 +47,7 @@ public sealed class OperatorBootstrapHostedService(
} }
} }
var ensuredLogin = await operatorBootstrapService.EnsureOperatorAsync( string? ensuredLogin = await operatorBootstrapService.EnsureOperatorAsync(
login, password, allowDevelopmentDefaults, ct); login, password, allowDevelopmentDefaults, ct);
if (ensuredLogin is not null) if (ensuredLogin is not null)
{ {
@@ -34,11 +34,11 @@ public sealed class TenantBootstrapService(IServiceScopeFactory scopeFactory) :
var authStore = scope.ServiceProvider.GetRequiredService<IAuthStore>(); var authStore = scope.ServiceProvider.GetRequiredService<IAuthStore>();
var passwordHasher = scope.ServiceProvider.GetRequiredService<IPasswordHasher>(); var passwordHasher = scope.ServiceProvider.GetRequiredService<IPasswordHasher>();
var login = NormalizeLogin(configuration[BootstrapLoginEnvKey] ?? DefaultAdminLogin); string login = NormalizeLogin(configuration[BootstrapLoginEnvKey] ?? DefaultAdminLogin);
var password = configuration[BootstrapPasswordEnvKey] ?? DefaultAdminPassword; string password = configuration[BootstrapPasswordEnvKey] ?? DefaultAdminPassword;
var environment = scope.ServiceProvider.GetRequiredService<IHostEnvironment>(); var environment = scope.ServiceProvider.GetRequiredService<IHostEnvironment>();
var seedDefaultTenant = environment.IsDevelopment() bool seedDefaultTenant = environment.IsDevelopment()
|| configuration[DefaultTenantBootstrapEnvKey] == DefaultTenantBootstrapEnabledValue; || configuration[DefaultTenantBootstrapEnvKey] == DefaultTenantBootstrapEnabledValue;
if (seedDefaultTenant) if (seedDefaultTenant)
{ {
@@ -26,8 +26,8 @@ public sealed class OperatorSessionMiddleware
/// </summary> /// </summary>
public async Task InvokeAsync(HttpContext context) public async Task InvokeAsync(HttpContext context)
{ {
var cookieName = _cookieOptions.CurrentValue.Name; string cookieName = _cookieOptions.CurrentValue.Name;
if (context.Request.Cookies.TryGetValue(cookieName, out var rawToken) if (context.Request.Cookies.TryGetValue(cookieName, out string? rawToken)
&& !string.IsNullOrWhiteSpace(rawToken)) && !string.IsNullOrWhiteSpace(rawToken))
{ {
// OperatorAuthService scoped: создаём scope на запрос через RequestServices. // OperatorAuthService scoped: создаём scope на запрос через RequestServices.
@@ -35,8 +35,8 @@ public sealed class SessionMiddleware
{ {
try try
{ {
var cookieName = _cookieOptions.CurrentValue.Name; string cookieName = _cookieOptions.CurrentValue.Name;
if (context.Request.Cookies.TryGetValue(cookieName, out var rawToken) if (context.Request.Cookies.TryGetValue(cookieName, out string? rawToken)
&& !string.IsNullOrWhiteSpace(rawToken)) && !string.IsNullOrWhiteSpace(rawToken))
{ {
// AuthService scoped: создаём scope на запрос через RequestServices. // AuthService scoped: создаём scope на запрос через RequestServices.
+1 -1
View File
@@ -69,7 +69,7 @@ DealMetricsHosting.AddDealMetrics(builder, metricsPort);
// Строка подключения Postgres — ТОЛЬКО из конфигурации (env/appsettings): dev-пароль в коде отсутствует // Строка подключения Postgres — ТОЛЬКО из конфигурации (env/appsettings): dev-пароль в коде отсутствует
// (Security review). Отсутствие строки = fail-fast на старте, а не тихий уход на несуществующую dev-БД. // (Security review). Отсутствие строки = fail-fast на старте, а не тихий уход на несуществующую dev-БД.
var connectionString = builder.Configuration.GetConnectionString("DealPostgres") string connectionString = builder.Configuration.GetConnectionString("DealPostgres")
?? throw new InvalidOperationException("ConnectionStrings:DealPostgres не задан"); ?? throw new InvalidOperationException("ConnectionStrings:DealPostgres не задан");
builder.Services.AddDbContext<DealDbContext>(options => options.UseNpgsql(connectionString)); builder.Services.AddDbContext<DealDbContext>(options => options.UseNpgsql(connectionString));
builder.Services.AddSingleton<ITenantContext, TenantContext>(); builder.Services.AddSingleton<ITenantContext, TenantContext>();
@@ -2,9 +2,7 @@ using Grpc.Core;
namespace Deal.Infrastructure.Integrations.Extensions; namespace Deal.Infrastructure.Integrations.Extensions;
/// <summary> // Расширения классификации gRPC-исключений клиентов автономных сервисов.
/// Расширения классификации gRPC-исключений клиентов автономных сервисов.
/// </summary>
internal static class RpcExceptionExtensions internal static class RpcExceptionExtensions
{ {
/// <summary> /// <summary>
@@ -3,9 +3,7 @@ using System.Net.Sockets;
namespace Deal.Infrastructure.Integrations.Extensions; namespace Deal.Infrastructure.Integrations.Extensions;
/// <summary> // Расширения Uri для SSRF-гейта интеграций
/// Расширения <see cref="Uri"/> для SSRF-гейта интеграций
/// </summary>
internal static class UriExtensions internal static class UriExtensions
{ {
/// <summary> /// <summary>
@@ -2,9 +2,7 @@ using Deal.Infrastructure.Integrations.Storage.Options;
namespace Deal.Infrastructure.Integrations.Storage.Extensions; namespace Deal.Infrastructure.Integrations.Storage.Extensions;
/// <summary> // Расширения MinioStorageOptions
/// Расширения <see cref="MinioStorageOptions"/>
/// </summary>
internal static class MinioStorageOptionsExtensions internal static class MinioStorageOptionsExtensions
{ {
/// <summary> /// <summary>
@@ -1,8 +1,6 @@
namespace Deal.Infrastructure.Integrations.Storage.Extensions; namespace Deal.Infrastructure.Integrations.Storage.Extensions;
/// <summary> // Расширения string для разбора конфигурационных значений.
/// Расширения <see cref="string"/> для разбора конфигурационных значений.
/// </summary>
internal static class StringExtensions internal static class StringExtensions
{ {
/// <summary> /// <summary>
@@ -10,7 +10,7 @@ public static class TenantSchemaMigrator
/// </summary> /// </summary>
public static string CreateSchemaSql(string schemaName) public static string CreateSchemaSql(string schemaName)
{ {
var escaped = schemaName.Replace("\"", "\"\""); string escaped = schemaName.Replace("\"", "\"\"");
return $"CREATE SCHEMA IF NOT EXISTS \"{escaped}\""; return $"CREATE SCHEMA IF NOT EXISTS \"{escaped}\"";
} }
@@ -10,7 +10,7 @@ public sealed class DealDbDesignTimeFactory : IDesignTimeDbContextFactory<DealDb
{ {
public DealDbContext CreateDbContext(string[] args) public DealDbContext CreateDbContext(string[] args)
{ {
var connectionString = Environment.GetEnvironmentVariable("DEAL_PG_CONNECTION") string connectionString = Environment.GetEnvironmentVariable("DEAL_PG_CONNECTION")
?? "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password"; ?? "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password";
var options = new DbContextOptionsBuilder<DealDbContext>() var options = new DbContextOptionsBuilder<DealDbContext>()
.UseNpgsql(connectionString) .UseNpgsql(connectionString)
@@ -10,7 +10,7 @@ public sealed class TenantDbDesignTimeFactory : IDesignTimeDbContextFactory<Tena
{ {
public TenantDbContext CreateDbContext(string[] args) public TenantDbContext CreateDbContext(string[] args)
{ {
var connectionString = Environment.GetEnvironmentVariable("DEAL_PG_CONNECTION") string connectionString = Environment.GetEnvironmentVariable("DEAL_PG_CONNECTION")
?? "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password"; ?? "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password";
var options = new DbContextOptionsBuilder<TenantDbContext>() var options = new DbContextOptionsBuilder<TenantDbContext>()
.UseNpgsql(connectionString, npgsql => npgsql.MigrationsHistoryTable("__TenantMigrationsHistory")) .UseNpgsql(connectionString, npgsql => npgsql.MigrationsHistoryTable("__TenantMigrationsHistory"))
@@ -24,7 +24,7 @@ public sealed class TenantProvisioningService(ConnectionStringProvider connectio
/// <inheritdoc /> /// <inheritdoc />
public async Task ProvisionAsync(TenantId tenantId, CancellationToken ct) public async Task ProvisionAsync(TenantId tenantId, CancellationToken ct)
{ {
var schemaName = tenantId.SchemaName; string schemaName = tenantId.SchemaName;
var provisionLock = SchemaProvisionLocks.GetOrAdd(schemaName, static _ => new SemaphoreSlim(1, 1)); var provisionLock = SchemaProvisionLocks.GetOrAdd(schemaName, static _ => new SemaphoreSlim(1, 1));
await provisionLock.WaitAsync(ct); await provisionLock.WaitAsync(ct);
try try
@@ -63,7 +63,7 @@ public sealed class TenantProvisioningService(ConnectionStringProvider connectio
{ {
// Строка уже с Search Path=tenant_&lt;id&gt; (ForSchemaDdl) — таблицы бессхемной модели TenantDbContext // Строка уже с Search Path=tenant_&lt;id&gt; (ForSchemaDdl) — таблицы бессхемной модели TenantDbContext
// лягут в схему тенанта. DDL применяется мигратор-ролью при её наличии (см. ConnectionStringProvider). // лягут в схему тенанта. DDL применяется мигратор-ролью при её наличии (см. ConnectionStringProvider).
var connectionString = connectionStringProvider.ForSchemaDdl(tenantId); string connectionString = connectionStringProvider.ForSchemaDdl(tenantId);
var options = new DbContextOptionsBuilder<TenantDbContext>() var options = new DbContextOptionsBuilder<TenantDbContext>()
.UseNpgsql(connectionString, npgsql => npgsql.MigrationsHistoryTable(TenantMigrationsHistoryTable, schemaName)) .UseNpgsql(connectionString, npgsql => npgsql.MigrationsHistoryTable(TenantMigrationsHistoryTable, schemaName))
.Options; .Options;
@@ -12,8 +12,14 @@ public interface IContainerRules
/// </summary> /// </summary>
public string Mode { get; } public string Mode { get; }
/// <summary>
/// Ключевые слова
/// </summary>
public IReadOnlyList<string> Keywords { get; } public IReadOnlyList<string> Keywords { get; }
/// <summary>
/// Стек технологий
/// </summary>
public IReadOnlyList<string> Stack { get; } public IReadOnlyList<string> Stack { get; }
/// <summary> /// <summary>
@@ -2,9 +2,7 @@ using Deal.Modules.Kanban.Application.Models;
namespace Deal.Modules.Kanban.Application.ColumnRules; namespace Deal.Modules.Kanban.Application.ColumnRules;
/// <summary> // Расширения бюджетной группы правил колонки.
/// Расширения бюджетной группы правил колонки.
/// </summary>
internal static class BudgetRangeDtoExtensions internal static class BudgetRangeDtoExtensions
{ {
/// <summary> /// <summary>
@@ -1,8 +1,6 @@
namespace Deal.Modules.Kanban.Application.ColumnRules; namespace Deal.Modules.Kanban.Application.ColumnRules;
/// <summary> // Расширения списков термов правил колонки.
/// Расширения списков термов правил колонки.
/// </summary>
internal static class TermListExtensions internal static class TermListExtensions
{ {
/// <summary> /// <summary>
@@ -1,8 +1,6 @@
namespace Deal.Modules.Kanban.Application.Extensions; namespace Deal.Modules.Kanban.Application.Extensions;
/// <summary> // Расширения символов для нормализации бюджетной валюты.
/// Расширения символов для нормализации бюджетной валюты.
/// </summary>
internal static class CharExtensions internal static class CharExtensions
{ {
/// <summary> /// <summary>
@@ -1,8 +1,6 @@
namespace Deal.Modules.Pipeline.Application.Parse; namespace Deal.Modules.Pipeline.Application.Parse;
/// <summary> // Расширения кодовых точек для чистки текста сообщений.
/// Расширения кодовых точек для чистки текста сообщений.
/// </summary>
internal static class CodePointExtensions internal static class CodePointExtensions
{ {
/// <summary> /// <summary>
@@ -1,8 +1,6 @@
namespace Deal.Modules.Pipeline.Application.Parse; namespace Deal.Modules.Pipeline.Application.Parse;
/// <summary> // Расширения строк для разбора текста сообщений.
/// Расширения строк для разбора текста сообщений.
/// </summary>
internal static class StringExtensions internal static class StringExtensions
{ {
private static readonly string[] FooterHintsArray = private static readonly string[] FooterHintsArray =
@@ -141,7 +141,7 @@ public static class SettingsKeys
public const string ExcludeTypes = "excludeTypes"; public const string ExcludeTypes = "excludeTypes";
// ── Dict / special ── // ── Словари / особые ──
public const string ColState = "colState"; public const string ColState = "colState";
@@ -1,8 +1,6 @@
namespace Deal.Modules.Tenants.Application.Models; namespace Deal.Modules.Tenants.Application.Models;
/// <summary> // Расширения записей аудита
/// Расширения записей аудита
/// </summary>
internal static class AuditRecordDtoExtensions internal static class AuditRecordDtoExtensions
{ {
// События аудита «неудачный вход» (тенант/оператор). // События аудита «неудачный вход» (тенант/оператор).
@@ -36,7 +36,7 @@ public sealed class AuthService(
string password, string password,
CancellationToken ct) CancellationToken ct)
{ {
var normalizedLogin = NormalizeLogin(login); string normalizedLogin = NormalizeLogin(login);
if (string.IsNullOrEmpty(normalizedLogin) || string.IsNullOrEmpty(password)) if (string.IsNullOrEmpty(normalizedLogin) || string.IsNullOrEmpty(password))
{ {
return new LoginResultDto(null, null); return new LoginResultDto(null, null);
@@ -61,7 +61,7 @@ public sealed class AuthService(
Error: LoginResultDto.ErrorTenantSuspended); Error: LoginResultDto.ErrorTenantSuspended);
} }
var token = await CreateSessionForUserAsync(user.Id, user.Login, impersonatedByOperatorId: null, ct); string token = await CreateSessionForUserAsync(user.Id, user.Login, impersonatedByOperatorId: null, ct);
return new LoginResultDto(user.Login, token, user.Id, user.TenantId); return new LoginResultDto(user.Login, token, user.Id, user.TenantId);
} }
@@ -77,7 +77,7 @@ public sealed class AuthService(
return null; return null;
} }
var tokenHash = SessionTokens.HashToken(rawToken); string tokenHash = SessionTokens.HashToken(rawToken);
var session = await authStore.FindSessionByTokenHashAsync(tokenHash, ct); var session = await authStore.FindSessionByTokenHashAsync(tokenHash, ct);
await authStore.DeleteSessionAsync(tokenHash, ct); await authStore.DeleteSessionAsync(tokenHash, ct);
@@ -111,7 +111,7 @@ public sealed class AuthService(
string newPassword, string newPassword,
CancellationToken ct) CancellationToken ct)
{ {
var normalizedLogin = NormalizeLogin(login); string normalizedLogin = NormalizeLogin(login);
var user = await authStore.FindUserByLoginAsync(normalizedLogin, ct); var user = await authStore.FindUserByLoginAsync(normalizedLogin, ct);
if (user is null || oldPassword is null || !passwordHasher.Verify(oldPassword, user.PasswordHash)) if (user is null || oldPassword is null || !passwordHasher.Verify(oldPassword, user.PasswordHash))
{ {
@@ -125,7 +125,7 @@ public sealed class AuthService(
await authStore.DeleteSessionsByUserIdAsync(user.Id, ct); await authStore.DeleteSessionsByUserIdAsync(user.Id, ct);
await authStore.UpdatePasswordHashAsync(user.Id, passwordHasher.Hash(newPassword), ct); await authStore.UpdatePasswordHashAsync(user.Id, passwordHasher.Hash(newPassword), ct);
var token = await CreateSessionForUserAsync(user.Id, user.Login, impersonatedByOperatorId: null, ct); string token = await CreateSessionForUserAsync(user.Id, user.Login, impersonatedByOperatorId: null, ct);
return new ChangePasswordResultDto(Ok: true, Error: null, NewToken: token); return new ChangePasswordResultDto(Ok: true, Error: null, NewToken: token);
} }
@@ -148,7 +148,7 @@ public sealed class AuthService(
return ImpersonationResultDto.Failed(ImpersonationResultDto.ErrorTenantNotFound); return ImpersonationResultDto.Failed(ImpersonationResultDto.ErrorTenantNotFound);
} }
var normalizedLogin = NormalizeLogin(targetLogin); string normalizedLogin = NormalizeLogin(targetLogin);
if (string.IsNullOrEmpty(normalizedLogin)) if (string.IsNullOrEmpty(normalizedLogin))
{ {
// login не задан — первый пользователь тенанта (по CreatedAt, порядок ListUsersByTenantIdAsync). // login не задан — первый пользователь тенанта (по CreatedAt, порядок ListUsersByTenantIdAsync).
@@ -159,7 +159,7 @@ public sealed class AuthService(
} }
UserIdentityDto firstUser = tenantUsers[0]; UserIdentityDto firstUser = tenantUsers[0];
var rawTokenForFirst = await CreateSessionForUserAsync(firstUser.Id, firstUser.Login, operatorId, ct); string rawTokenForFirst = await CreateSessionForUserAsync(firstUser.Id, firstUser.Login, operatorId, ct);
return Success(rawTokenForFirst, firstUser.Id, firstUser.Login, firstUser.TenantId); return Success(rawTokenForFirst, firstUser.Id, firstUser.Login, firstUser.TenantId);
} }
@@ -170,7 +170,7 @@ public sealed class AuthService(
return ImpersonationResultDto.Failed(ImpersonationResultDto.ErrorUserNotFound); return ImpersonationResultDto.Failed(ImpersonationResultDto.ErrorUserNotFound);
} }
var rawToken = await CreateSessionForUserAsync(user.Id, user.Login, operatorId, ct); string rawToken = await CreateSessionForUserAsync(user.Id, user.Login, operatorId, ct);
return Success(rawToken, user.Id, user.Login, user.TenantId); return Success(rawToken, user.Id, user.Login, user.TenantId);
} }
@@ -186,7 +186,7 @@ public sealed class AuthService(
return null; return null;
} }
var tokenHash = SessionTokens.HashToken(rawToken); string tokenHash = SessionTokens.HashToken(rawToken);
var session = await authStore.FindSessionByTokenHashAsync(tokenHash, ct); var session = await authStore.FindSessionByTokenHashAsync(tokenHash, ct);
UserIdentityDto? user = null; UserIdentityDto? user = null;
if (session is not null && session.ExpiresAt > DateTimeOffset.UtcNow) if (session is not null && session.ExpiresAt > DateTimeOffset.UtcNow)
@@ -230,7 +230,7 @@ public sealed class AuthService(
Guid? impersonatedByOperatorId, Guid? impersonatedByOperatorId,
CancellationToken ct) CancellationToken ct)
{ {
var rawToken = SessionTokens.NewToken(); string rawToken = SessionTokens.NewToken();
var session = new SessionDto( var session = new SessionDto(
TokenHash: SessionTokens.HashToken(rawToken), TokenHash: SessionTokens.HashToken(rawToken),
UserId: userId, UserId: userId,
@@ -27,7 +27,7 @@ public sealed class OperatorAuthService(IOperatorAuthStore operatorAuthStore, IP
string password, string password,
CancellationToken ct) CancellationToken ct)
{ {
var normalizedLogin = NormalizeLogin(login); string normalizedLogin = NormalizeLogin(login);
if (string.IsNullOrEmpty(normalizedLogin) || string.IsNullOrEmpty(password)) if (string.IsNullOrEmpty(normalizedLogin) || string.IsNullOrEmpty(password))
{ {
return new OperatorLoginResultDto(null, null); return new OperatorLoginResultDto(null, null);
@@ -39,7 +39,7 @@ public sealed class OperatorAuthService(IOperatorAuthStore operatorAuthStore, IP
return new OperatorLoginResultDto(null, null); return new OperatorLoginResultDto(null, null);
} }
var token = await CreateSessionForOperatorAsync(operatorRecord, ct); string token = await CreateSessionForOperatorAsync(operatorRecord, ct);
return new OperatorLoginResultDto(operatorRecord.Login, token, operatorRecord.Id); return new OperatorLoginResultDto(operatorRecord.Login, token, operatorRecord.Id);
} }
@@ -69,7 +69,7 @@ public sealed class OperatorAuthService(IOperatorAuthStore operatorAuthStore, IP
return null; return null;
} }
var tokenHash = SessionTokens.HashToken(rawToken); string tokenHash = SessionTokens.HashToken(rawToken);
var session = await operatorAuthStore.FindSessionByTokenHashAsync(tokenHash, ct); var session = await operatorAuthStore.FindSessionByTokenHashAsync(tokenHash, ct);
OperatorIdentityDto? operatorIdentity = null; OperatorIdentityDto? operatorIdentity = null;
if (session is not null && session.ExpiresAt > DateTimeOffset.UtcNow) if (session is not null && session.ExpiresAt > DateTimeOffset.UtcNow)
@@ -83,7 +83,7 @@ public sealed class OperatorAuthService(IOperatorAuthStore operatorAuthStore, IP
else if (session is not null) else if (session is not null)
{ {
// Очистка протухших сессий — только при обнаружении протухшей (редкий случай), не на каждый запрос // Очистка протухших сессий — только при обнаружении протухшей (редкий случай), не на каждый запрос
// (hot-path, Security review). // (горячий путь запросов).
await operatorAuthStore.DeleteExpiredSessionsAsync(ct); await operatorAuthStore.DeleteExpiredSessionsAsync(ct);
} }
@@ -96,7 +96,7 @@ public sealed class OperatorAuthService(IOperatorAuthStore operatorAuthStore, IP
// Возвращает: Raw-токен для выдачи клиенту. // Возвращает: Raw-токен для выдачи клиенту.
private async Task<string> CreateSessionForOperatorAsync(StoredOperatorDto operatorRecord, CancellationToken ct) private async Task<string> CreateSessionForOperatorAsync(StoredOperatorDto operatorRecord, CancellationToken ct)
{ {
var rawToken = SessionTokens.NewToken(); string rawToken = SessionTokens.NewToken();
var session = new OperatorSessionDto( var session = new OperatorSessionDto(
TokenHash: SessionTokens.HashToken(rawToken), TokenHash: SessionTokens.HashToken(rawToken),
OperatorId: operatorRecord.Id, OperatorId: operatorRecord.Id,
@@ -43,8 +43,8 @@ public sealed class OperatorBootstrapService(IOperatorAuthStore operatorAuthStor
bool allowDevelopmentDefaults, bool allowDevelopmentDefaults,
CancellationToken ct) CancellationToken ct)
{ {
var normalizedLogin = NormalizeLogin(login); string normalizedLogin = NormalizeLogin(login);
var resolvedPassword = password; string? resolvedPassword = password;
if (string.IsNullOrEmpty(normalizedLogin) || string.IsNullOrEmpty(resolvedPassword)) if (string.IsNullOrEmpty(normalizedLogin) || string.IsNullOrEmpty(resolvedPassword))
{ {
if (!allowDevelopmentDefaults) if (!allowDevelopmentDefaults)
@@ -26,7 +26,7 @@ public static class SessionTokens
/// <returns>64 hex-символа.</returns> /// <returns>64 hex-символа.</returns>
public static string HashToken(string rawToken) public static string HashToken(string rawToken)
{ {
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(rawToken)); byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(rawToken));
return Convert.ToHexString(hash).ToLowerInvariant(); return Convert.ToHexString(hash).ToLowerInvariant();
} }
} }
@@ -36,7 +36,7 @@ public sealed class TenantService(ITenantRepository tenantRepository, ITenantPro
CreatedAt: DateTimeOffset.UtcNow), CreatedAt: DateTimeOffset.UtcNow),
ct); ct);
var tenantIdValue = id.ToString("N"); string tenantIdValue = id.ToString("N");
await tenantProvisioner.ProvisionAsync(new TenantId(tenantIdValue), ct); await tenantProvisioner.ProvisionAsync(new TenantId(tenantIdValue), ct);
return new TenantId(tenantIdValue); return new TenantId(tenantIdValue);
} }
@@ -7,8 +7,14 @@ namespace Deal.SharedKernel.Tenants.Abstractions;
/// </summary> /// </summary>
public interface ITenantContext public interface ITenantContext
{ {
/// <summary>
/// Идентификатор текущего тенанта или null для системного контекста
/// </summary>
public TenantId? TenantId { get; } public TenantId? TenantId { get; }
/// <summary>
/// Признак наличия текущего тенанта
/// </summary>
public bool HasTenant { get; } public bool HasTenant { get; }
/// <summary> /// <summary>
@@ -18,7 +18,7 @@ public static class UrlSafeToken
{ {
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(byteCount); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(byteCount);
var bytes = new byte[byteCount]; byte[] bytes = new byte[byteCount];
RandomNumberGenerator.Fill(bytes); RandomNumberGenerator.Fill(bytes);
return Convert.ToBase64String(bytes) return Convert.ToBase64String(bytes)
.TrimEnd('=') .TrimEnd('=')
@@ -37,7 +37,7 @@ public sealed class AuthStoreTests
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
await CreateUserAsync(store, Guid.NewGuid(), "user@example.com"); await CreateUserAsync(store, Guid.NewGuid(), "user@example.com");
var operatorId = Guid.NewGuid(); var operatorId = Guid.NewGuid();
var tokenHash = "token-hash-impersonation"; string tokenHash = "token-hash-impersonation";
await store.CreateSessionAsync( await store.CreateSessionAsync(
new SessionDto(tokenHash, userId, "user@example.com", DateTimeOffset.UtcNow.AddDays(30), operatorId), new SessionDto(tokenHash, userId, "user@example.com", DateTimeOffset.UtcNow.AddDays(30), operatorId),
@@ -49,7 +49,7 @@ public sealed class AuthStoreTests
Assert.Equal(userId, found.UserId); Assert.Equal(userId, found.UserId);
// Обычная сессия (без маркера) читается как ImpersonatedByOperatorId=null. // Обычная сессия (без маркера) читается как ImpersonatedByOperatorId=null.
var plainHash = "token-hash-plain"; string plainHash = "token-hash-plain";
await store.CreateSessionAsync( await store.CreateSessionAsync(
new SessionDto(plainHash, userId, "user@example.com", DateTimeOffset.UtcNow.AddDays(30)), new SessionDto(plainHash, userId, "user@example.com", DateTimeOffset.UtcNow.AddDays(30)),
CancellationToken.None); CancellationToken.None);
@@ -53,7 +53,7 @@ public sealed class TenantSchemaMigrationServiceTests
[Fact] [Fact]
public async Task MigrateAllAsync_OneSchemaFails_ContinuesAndReportsFailed() public async Task MigrateAllAsync_OneSchemaFails_ContinuesAndReportsFailed()
{ {
var failingSchema = $"tenant_{TenantA:N}"; string failingSchema = $"tenant_{TenantA:N}";
var provisioner = new FailingTenantProvisioner(failingSchema); var provisioner = new FailingTenantProvisioner(failingSchema);
var repository = new FakeTenantRepository( var repository = new FakeTenantRepository(
TenantRecord(TenantA, "A"), TenantRecord(TenantA, "A"),
@@ -115,7 +115,7 @@ public sealed class TenantSchemaMigrationServiceTests
public async Task MigrateAllAsync_SchemaFailsWithinPage_ContinuesOthers() public async Task MigrateAllAsync_SchemaFailsWithinPage_ContinuesOthers()
{ {
TenantRecordDto[] records = Records(3); TenantRecordDto[] records = Records(3);
var failingSchema = $"tenant_{records[0].Id:N}"; string failingSchema = $"tenant_{records[0].Id:N}";
var provisioner = new FailingTenantProvisioner(failingSchema); var provisioner = new FailingTenantProvisioner(failingSchema);
var repository = new FakeTenantRepository(records); var repository = new FakeTenantRepository(records);
var service = NewService(repository, provisioner); var service = NewService(repository, provisioner);
@@ -7,7 +7,7 @@ public sealed class TenantSchemaMigratorTests
[Fact] [Fact]
public void CreateSchemaSql_IsEscaped() public void CreateSchemaSql_IsEscaped()
{ {
var sql = TenantSchemaMigrator.CreateSchemaSql("tenant_abc"); string sql = TenantSchemaMigrator.CreateSchemaSql("tenant_abc");
Assert.Contains("CREATE SCHEMA IF NOT EXISTS \"tenant_abc\"", sql); Assert.Contains("CREATE SCHEMA IF NOT EXISTS \"tenant_abc\"", sql);
Assert.DoesNotContain("; DROP", sql); Assert.DoesNotContain("; DROP", sql);
} }
@@ -15,7 +15,7 @@ public sealed class TenantSchemaMigratorTests
[Fact] [Fact]
public void CreateSchemaSql_EscapesQuotes() public void CreateSchemaSql_EscapesQuotes()
{ {
var sql = TenantSchemaMigrator.CreateSchemaSql("tenant_a\"b"); string sql = TenantSchemaMigrator.CreateSchemaSql("tenant_a\"b");
Assert.Contains("\"tenant_a\"\"b\"", sql); Assert.Contains("\"tenant_a\"\"b\"", sql);
Assert.DoesNotContain("\"tenant_a\"b\"", sql); Assert.DoesNotContain("\"tenant_a\"b\"", sql);
} }
@@ -91,7 +91,7 @@ public sealed class PromptDefaultsTests
[Fact] [Fact]
public void Fill_MoreThanSixtyKeywords_TakesFirstSixty() public void Fill_MoreThanSixtyKeywords_TakesFirstSixty()
{ {
var keywords = Enumerable.Range(1, 65).Select(index => $"слово-{index}").ToArray(); string[] keywords = Enumerable.Range(1, 65).Select(index => $"слово-{index}").ToArray();
string result = PromptFiller.Fill("{keywords}", "сфера", keywords); string result = PromptFiller.Fill("{keywords}", "сфера", keywords);
@@ -125,7 +125,7 @@ public sealed class AuthServiceTests
[Fact] [Fact]
public async Task ResolveSessionAsync_WithExpiredSession_ReturnsNull() public async Task ResolveSessionAsync_WithExpiredSession_ReturnsNull()
{ {
var rawToken = "expired-session-raw-token"; string rawToken = "expired-session-raw-token";
_store.AddSession(new SessionDto( _store.AddSession(new SessionDto(
SessionTokens.HashToken(rawToken), UserId, UserLogin, DateTimeOffset.UtcNow.AddDays(-1))); SessionTokens.HashToken(rawToken), UserId, UserLogin, DateTimeOffset.UtcNow.AddDays(-1)));
@@ -140,7 +140,7 @@ public sealed class AuthServiceTests
[Fact] [Fact]
public async Task ResolveSessionAsync_WithValidSession_ReturnsUser() public async Task ResolveSessionAsync_WithValidSession_ReturnsUser()
{ {
var rawToken = "valid-session-raw-token"; string rawToken = "valid-session-raw-token";
_store.AddSession(new SessionDto( _store.AddSession(new SessionDto(
SessionTokens.HashToken(rawToken), UserId, UserLogin, DateTimeOffset.UtcNow.AddDays(30))); SessionTokens.HashToken(rawToken), UserId, UserLogin, DateTimeOffset.UtcNow.AddDays(30)));
@@ -157,7 +157,7 @@ public sealed class AuthServiceTests
[Fact] [Fact]
public async Task ResolveSessionAsync_WhenTenantSuspended_ReturnsNullImmediately() public async Task ResolveSessionAsync_WhenTenantSuspended_ReturnsNullImmediately()
{ {
var rawToken = "suspended-tenant-session"; string rawToken = "suspended-tenant-session";
_store.AddSession(new SessionDto( _store.AddSession(new SessionDto(
SessionTokens.HashToken(rawToken), UserId, UserLogin, DateTimeOffset.UtcNow.AddDays(30))); SessionTokens.HashToken(rawToken), UserId, UserLogin, DateTimeOffset.UtcNow.AddDays(30)));
await _tenantStore.UpdateStatusAsync(UserTenantId, TenantStatuses.Suspended, CancellationToken.None); await _tenantStore.UpdateStatusAsync(UserTenantId, TenantStatuses.Suspended, CancellationToken.None);
@@ -181,8 +181,8 @@ public sealed class AuthServiceTests
[Fact] [Fact]
public async Task LogoutAsync_WithToken_DeletesSession() public async Task LogoutAsync_WithToken_DeletesSession()
{ {
var rawToken = "logout-session-raw-token"; string rawToken = "logout-session-raw-token";
var tokenHash = SessionTokens.HashToken(rawToken); string tokenHash = SessionTokens.HashToken(rawToken);
_store.AddSession(new SessionDto(tokenHash, UserId, UserLogin, DateTimeOffset.UtcNow.AddDays(30))); _store.AddSession(new SessionDto(tokenHash, UserId, UserLogin, DateTimeOffset.UtcNow.AddDays(30)));
await _service.LogoutAsync(rawToken, CancellationToken.None); await _service.LogoutAsync(rawToken, CancellationToken.None);
@@ -330,8 +330,8 @@ public sealed class AuthServiceTests
public async Task LogoutAsync_ForImpersonationSession_ReturnsStopInfoAndDeletesSession() public async Task LogoutAsync_ForImpersonationSession_ReturnsStopInfoAndDeletesSession()
{ {
var operatorId = Guid.NewGuid(); var operatorId = Guid.NewGuid();
var rawToken = "impersonation-session-raw-token"; string rawToken = "impersonation-session-raw-token";
var tokenHash = SessionTokens.HashToken(rawToken); string tokenHash = SessionTokens.HashToken(rawToken);
_store.AddSession(new SessionDto(tokenHash, UserId, UserLogin, DateTimeOffset.UtcNow.AddDays(30), operatorId)); _store.AddSession(new SessionDto(tokenHash, UserId, UserLogin, DateTimeOffset.UtcNow.AddDays(30), operatorId));
var logout = await _service.LogoutAsync(rawToken, CancellationToken.None); var logout = await _service.LogoutAsync(rawToken, CancellationToken.None);
@@ -347,8 +347,8 @@ public sealed class AuthServiceTests
[Fact] [Fact]
public async Task LogoutAsync_ForNormalSession_ReturnsNull() public async Task LogoutAsync_ForNormalSession_ReturnsNull()
{ {
var rawToken = "normal-session-raw-token"; string rawToken = "normal-session-raw-token";
var tokenHash = SessionTokens.HashToken(rawToken); string tokenHash = SessionTokens.HashToken(rawToken);
_store.AddSession(new SessionDto(tokenHash, UserId, UserLogin, DateTimeOffset.UtcNow.AddDays(30))); _store.AddSession(new SessionDto(tokenHash, UserId, UserLogin, DateTimeOffset.UtcNow.AddDays(30)));
var logout = await _service.LogoutAsync(rawToken, CancellationToken.None); var logout = await _service.LogoutAsync(rawToken, CancellationToken.None);
@@ -102,7 +102,7 @@ public sealed class FakeAuthStore : IAuthStore
string passwordHash, string passwordHash,
CancellationToken ct) CancellationToken ct)
{ {
var index = _users.FindIndex(u => u.Id == userId); int index = _users.FindIndex(u => u.Id == userId);
if (index >= 0) if (index >= 0)
{ {
var user = _users[index]; var user = _users[index];
@@ -71,7 +71,7 @@ public sealed class OperatorAuthServiceTests
[Fact] [Fact]
public async Task ResolveSessionAsync_WithValidSession_ReturnsOperatorIdentity() public async Task ResolveSessionAsync_WithValidSession_ReturnsOperatorIdentity()
{ {
var rawToken = "valid-operator-session-raw-token"; string rawToken = "valid-operator-session-raw-token";
_store.AddSession(new OperatorSessionDto( _store.AddSession(new OperatorSessionDto(
SessionTokens.HashToken(rawToken), OperatorId, OperatorLogin, DateTimeOffset.UtcNow.AddHours(12))); SessionTokens.HashToken(rawToken), OperatorId, OperatorLogin, DateTimeOffset.UtcNow.AddHours(12)));
@@ -89,7 +89,7 @@ public sealed class OperatorAuthServiceTests
[Fact] [Fact]
public async Task ResolveSessionAsync_WithExpiredSession_ReturnsNull() public async Task ResolveSessionAsync_WithExpiredSession_ReturnsNull()
{ {
var rawToken = "expired-operator-session-raw-token"; string rawToken = "expired-operator-session-raw-token";
_store.AddSession(new OperatorSessionDto( _store.AddSession(new OperatorSessionDto(
SessionTokens.HashToken(rawToken), OperatorId, OperatorLogin, DateTimeOffset.UtcNow.AddHours(-1))); SessionTokens.HashToken(rawToken), OperatorId, OperatorLogin, DateTimeOffset.UtcNow.AddHours(-1)));
@@ -104,7 +104,7 @@ public sealed class OperatorAuthServiceTests
[Fact] [Fact]
public async Task ResolveSessionAsync_WhenOperatorWasDeleted_ReturnsNull() public async Task ResolveSessionAsync_WhenOperatorWasDeleted_ReturnsNull()
{ {
var rawToken = "orphan-operator-session-raw-token"; string rawToken = "orphan-operator-session-raw-token";
_store.AddSession(new OperatorSessionDto( _store.AddSession(new OperatorSessionDto(
SessionTokens.HashToken(rawToken), Guid.NewGuid(), "deleted-operator", DateTimeOffset.UtcNow.AddHours(12))); SessionTokens.HashToken(rawToken), Guid.NewGuid(), "deleted-operator", DateTimeOffset.UtcNow.AddHours(12)));
@@ -119,7 +119,7 @@ public sealed class OperatorAuthServiceTests
const string suspendedLogin = "suspended-operator"; const string suspendedLogin = "suspended-operator";
var passwordHasher = new FakePasswordHasher(); var passwordHasher = new FakePasswordHasher();
_store.AddOperator(new StoredOperatorDto(Guid.NewGuid(), suspendedLogin, "suspended", passwordHasher.Hash("x"))); _store.AddOperator(new StoredOperatorDto(Guid.NewGuid(), suspendedLogin, "suspended", passwordHasher.Hash("x")));
var rawToken = "suspended-operator-session-raw-token"; string rawToken = "suspended-operator-session-raw-token";
_store.AddSession(new OperatorSessionDto( _store.AddSession(new OperatorSessionDto(
SessionTokens.HashToken(rawToken), Guid.NewGuid(), suspendedLogin, DateTimeOffset.UtcNow.AddHours(12))); SessionTokens.HashToken(rawToken), Guid.NewGuid(), suspendedLogin, DateTimeOffset.UtcNow.AddHours(12)));
@@ -141,8 +141,8 @@ public sealed class OperatorAuthServiceTests
[Fact] [Fact]
public async Task LogoutAsync_WithToken_DeletesSession() public async Task LogoutAsync_WithToken_DeletesSession()
{ {
var rawToken = "logout-operator-session-raw-token"; string rawToken = "logout-operator-session-raw-token";
var tokenHash = SessionTokens.HashToken(rawToken); string tokenHash = SessionTokens.HashToken(rawToken);
_store.AddSession(new OperatorSessionDto(tokenHash, OperatorId, OperatorLogin, DateTimeOffset.UtcNow.AddHours(12))); _store.AddSession(new OperatorSessionDto(tokenHash, OperatorId, OperatorLogin, DateTimeOffset.UtcNow.AddHours(12)));
await _service.LogoutAsync(rawToken, CancellationToken.None); await _service.LogoutAsync(rawToken, CancellationToken.None);
@@ -22,7 +22,7 @@ public sealed class OperatorBootstrapServiceTests
[Fact] [Fact]
public async Task InDevelopment_WithoutEnvCredentials_SeedsDefaultOperator() public async Task InDevelopment_WithoutEnvCredentials_SeedsDefaultOperator()
{ {
var login = await _service.EnsureOperatorAsync(null, null, allowDevelopmentDefaults: true, CancellationToken.None); string? login = await _service.EnsureOperatorAsync(null, null, allowDevelopmentDefaults: true, CancellationToken.None);
Assert.Equal(OperatorBootstrapService.DefaultOperatorLogin, login); Assert.Equal(OperatorBootstrapService.DefaultOperatorLogin, login);
@@ -36,7 +36,7 @@ public sealed class OperatorBootstrapServiceTests
[Fact] [Fact]
public async Task InDevelopment_WithEmptyEnvCredentials_SeedsDefaultOperator() public async Task InDevelopment_WithEmptyEnvCredentials_SeedsDefaultOperator()
{ {
var login = await _service.EnsureOperatorAsync("", "", allowDevelopmentDefaults: true, CancellationToken.None); string? login = await _service.EnsureOperatorAsync("", "", allowDevelopmentDefaults: true, CancellationToken.None);
Assert.Equal(OperatorBootstrapService.DefaultOperatorLogin, login); Assert.Equal(OperatorBootstrapService.DefaultOperatorLogin, login);
Assert.Single(_store.Operators); Assert.Single(_store.Operators);
@@ -45,8 +45,8 @@ public sealed class OperatorBootstrapServiceTests
[Fact] [Fact]
public async Task RepeatedBootstrap_IsIdempotent_KeepsSingleOperator() public async Task RepeatedBootstrap_IsIdempotent_KeepsSingleOperator()
{ {
var first = await _service.EnsureOperatorAsync(null, null, allowDevelopmentDefaults: true, CancellationToken.None); string? first = await _service.EnsureOperatorAsync(null, null, allowDevelopmentDefaults: true, CancellationToken.None);
var second = await _service.EnsureOperatorAsync(null, null, allowDevelopmentDefaults: true, CancellationToken.None); string? second = await _service.EnsureOperatorAsync(null, null, allowDevelopmentDefaults: true, CancellationToken.None);
Assert.Equal(OperatorBootstrapService.DefaultOperatorLogin, first); Assert.Equal(OperatorBootstrapService.DefaultOperatorLogin, first);
Assert.Equal(OperatorBootstrapService.DefaultOperatorLogin, second); Assert.Equal(OperatorBootstrapService.DefaultOperatorLogin, second);
@@ -58,7 +58,7 @@ public sealed class OperatorBootstrapServiceTests
[Fact] [Fact]
public async Task Production_WithoutEnvCredentials_SkipsBootstrap() public async Task Production_WithoutEnvCredentials_SkipsBootstrap()
{ {
var login = await _service.EnsureOperatorAsync(null, null, allowDevelopmentDefaults: false, CancellationToken.None); string? login = await _service.EnsureOperatorAsync(null, null, allowDevelopmentDefaults: false, CancellationToken.None);
Assert.Null(login); Assert.Null(login);
Assert.Empty(_store.Operators); Assert.Empty(_store.Operators);
@@ -68,7 +68,7 @@ public sealed class OperatorBootstrapServiceTests
[Fact] [Fact]
public async Task Production_WithEnvCredentials_CreatesOperatorWithNormalizedLogin() public async Task Production_WithEnvCredentials_CreatesOperatorWithNormalizedLogin()
{ {
var login = await _service.EnsureOperatorAsync( string? login = await _service.EnsureOperatorAsync(
" Root-Admin ", "root-password", allowDevelopmentDefaults: false, CancellationToken.None); " Root-Admin ", "root-password", allowDevelopmentDefaults: false, CancellationToken.None);
Assert.Equal("root-admin", login); Assert.Equal("root-admin", login);
@@ -83,7 +83,7 @@ public sealed class OperatorBootstrapServiceTests
{ {
_store.AddOperator(new StoredOperatorDto(Guid.NewGuid(), "existing-op", "active", _passwordHasher.Hash("old-password"))); _store.AddOperator(new StoredOperatorDto(Guid.NewGuid(), "existing-op", "active", _passwordHasher.Hash("old-password")));
var login = await _service.EnsureOperatorAsync("EXISTING-OP", "new-password", allowDevelopmentDefaults: false, CancellationToken.None); string? login = await _service.EnsureOperatorAsync("EXISTING-OP", "new-password", allowDevelopmentDefaults: false, CancellationToken.None);
Assert.Equal("existing-op", login); Assert.Equal("existing-op", login);
Assert.Single(_store.Operators); Assert.Single(_store.Operators);
@@ -13,7 +13,7 @@ public sealed class PasswordHasherTests
[Fact] [Fact]
public void Hash_ReturnsArgon2idEncodedStringDifferentFromPassword() public void Hash_ReturnsArgon2idEncodedStringDifferentFromPassword()
{ {
var hash = _hasher.Hash("correct horse battery staple"); string hash = _hasher.Hash("correct horse battery staple");
Assert.False(string.IsNullOrWhiteSpace(hash)); Assert.False(string.IsNullOrWhiteSpace(hash));
Assert.StartsWith("$argon2id$", hash); Assert.StartsWith("$argon2id$", hash);
@@ -23,7 +23,7 @@ public sealed class PasswordHasherTests
[Fact] [Fact]
public void Verify_WithCorrectPassword_ReturnsTrue() public void Verify_WithCorrectPassword_ReturnsTrue()
{ {
var hash = _hasher.Hash("secret42"); string hash = _hasher.Hash("secret42");
Assert.True(_hasher.Verify("secret42", hash)); Assert.True(_hasher.Verify("secret42", hash));
} }
@@ -31,7 +31,7 @@ public sealed class PasswordHasherTests
[Fact] [Fact]
public void Verify_WithWrongPassword_ReturnsFalse() public void Verify_WithWrongPassword_ReturnsFalse()
{ {
var hash = _hasher.Hash("secret42"); string hash = _hasher.Hash("secret42");
Assert.False(_hasher.Verify("wrong-password", hash)); Assert.False(_hasher.Verify("wrong-password", hash));
} }
@@ -39,8 +39,8 @@ public sealed class PasswordHasherTests
[Fact] [Fact]
public void Hash_SamePasswordTwice_DifferentHashesBecauseOfRandomSalt() public void Hash_SamePasswordTwice_DifferentHashesBecauseOfRandomSalt()
{ {
var first = _hasher.Hash("same-password"); string first = _hasher.Hash("same-password");
var second = _hasher.Hash("same-password"); string second = _hasher.Hash("same-password");
Assert.NotEqual(first, second); Assert.NotEqual(first, second);
Assert.True(_hasher.Verify("same-password", first)); Assert.True(_hasher.Verify("same-password", first));
@@ -10,8 +10,8 @@ public sealed class SessionTokensTests
[Fact] [Fact]
public void NewToken_ReturnsUniqueLongEnoughTokens() public void NewToken_ReturnsUniqueLongEnoughTokens()
{ {
var first = SessionTokens.NewToken(); string first = SessionTokens.NewToken();
var second = SessionTokens.NewToken(); string second = SessionTokens.NewToken();
Assert.NotEqual(first, second); Assert.NotEqual(first, second);
Assert.True(first.Length > 32); Assert.True(first.Length > 32);
@@ -21,7 +21,7 @@ public sealed class SessionTokensTests
[Fact] [Fact]
public void NewToken_UsesBase64UrlAlphabetWithoutPadding() public void NewToken_UsesBase64UrlAlphabetWithoutPadding()
{ {
var token = SessionTokens.NewToken(); string token = SessionTokens.NewToken();
// 32 байта в Base64Url без padding: 43 символа, без '=' и без небезопасных '+'/ '/'. // 32 байта в Base64Url без padding: 43 символа, без '=' и без небезопасных '+'/ '/'.
Assert.Equal(43, token.Length); Assert.Equal(43, token.Length);
@@ -33,8 +33,8 @@ public sealed class SessionTokensTests
[Fact] [Fact]
public void HashToken_IsDeterministicHexSha256() public void HashToken_IsDeterministicHexSha256()
{ {
var first = SessionTokens.HashToken("raw-token-value"); string first = SessionTokens.HashToken("raw-token-value");
var second = SessionTokens.HashToken("raw-token-value"); string second = SessionTokens.HashToken("raw-token-value");
Assert.Equal(first, second); Assert.Equal(first, second);
Assert.Equal(64, first.Length); Assert.Equal(64, first.Length);
@@ -43,8 +43,8 @@ public sealed class SessionTokensTests
[Fact] [Fact]
public void HashToken_DifferentTokensProduceDifferentHashes() public void HashToken_DifferentTokensProduceDifferentHashes()
{ {
var first = SessionTokens.HashToken("raw-token-1"); string first = SessionTokens.HashToken("raw-token-1");
var second = SessionTokens.HashToken("raw-token-2"); string second = SessionTokens.HashToken("raw-token-2");
Assert.NotEqual(first, second); Assert.NotEqual(first, second);
} }
@@ -41,7 +41,7 @@ public sealed class ConversionRecomputerTests
int updated = await service.RecomputeAsync(CancellationToken.None); int updated = await service.RecomputeAsync(CancellationToken.None);
// 100 USD → EUR: 100 * 92.5 / 99.9 = 92.5925… → round 2. // 100 USD → EUR: 100 * 92.5 / 99.9 = 92.5925… → округление до 2 знаков.
Assert.Equal(1, updated); Assert.Equal(1, updated);
AssertConverted(store, "l_board", convFrom: 92.59, convTo: 92.59, "EUR"); AssertConverted(store, "l_board", convFrom: 92.59, convTo: 92.59, "EUR");
} }
@@ -1,10 +1,10 @@
using System.Diagnostics; using System.Diagnostics;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Logging;
using Deal.Grpc.Hosting.Models; using Deal.Grpc.Hosting.Models;
using Deal.Grpc.Hosting.Options; using Deal.Grpc.Hosting.Options;
using Deal.Grpc.Hosting.Services; using Deal.Grpc.Hosting.Services;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Logging;
namespace Deal.Grpc.Hosting.Interceptors; namespace Deal.Grpc.Hosting.Interceptors;
@@ -1,11 +1,11 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Configuration;
using Deal.Grpc.Hosting.Models; using Deal.Grpc.Hosting.Models;
using Deal.Grpc.Hosting.Options; using Deal.Grpc.Hosting.Options;
using Deal.Grpc.Hosting.Services; using Deal.Grpc.Hosting.Services;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Configuration;
namespace Deal.Grpc.Hosting.Interceptors; namespace Deal.Grpc.Hosting.Interceptors;
@@ -1,7 +1,7 @@
using Microsoft.Extensions.Configuration;
using Deal.Grpc.Hosting.Interceptors; using Deal.Grpc.Hosting.Interceptors;
using Deal.Grpc.Hosting.Models; using Deal.Grpc.Hosting.Models;
using Deal.Grpc.Hosting.Services; using Deal.Grpc.Hosting.Services;
using Microsoft.Extensions.Configuration;
namespace Deal.Grpc.Hosting.Options; namespace Deal.Grpc.Hosting.Options;
@@ -1,12 +1,12 @@
using Deal.Grpc.Hosting.Interceptors;
using Deal.Grpc.Hosting.Models;
using Deal.Grpc.Hosting.Options;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Serilog; using Serilog;
using Serilog.Events; using Serilog.Events;
using Serilog.Formatting.Compact; using Serilog.Formatting.Compact;
using Deal.Grpc.Hosting.Interceptors;
using Deal.Grpc.Hosting.Models;
using Deal.Grpc.Hosting.Options;
namespace Deal.Grpc.Hosting.Services; namespace Deal.Grpc.Hosting.Services;
@@ -1,11 +1,11 @@
using Deal.Grpc.Hosting.Interceptors;
using Deal.Grpc.Hosting.Models;
using Deal.Grpc.Hosting.Options;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Metrics; using OpenTelemetry.Metrics;
using Deal.Grpc.Hosting.Interceptors;
using Deal.Grpc.Hosting.Models;
using Deal.Grpc.Hosting.Options;
namespace Deal.Grpc.Hosting.Services; namespace Deal.Grpc.Hosting.Services;
@@ -1,13 +1,13 @@
using System.Net; using System.Net;
using Deal.Grpc.Hosting.Interceptors;
using Deal.Grpc.Hosting.Models;
using Deal.Grpc.Hosting.Options;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks;
using Deal.Grpc.Hosting.Interceptors;
using Deal.Grpc.Hosting.Models;
using Deal.Grpc.Hosting.Options;
namespace Deal.Grpc.Hosting.Services; namespace Deal.Grpc.Hosting.Services;
@@ -2,9 +2,7 @@ using Deal.Ml.Model;
namespace Deal.Ml.Extensions; namespace Deal.Ml.Extensions;
/// <summary> // Расширения string для меток классов ML-модели.
/// Расширения <see cref="string"/> для меток классов ML-модели.
/// </summary>
internal static class LabelExtensions internal static class LabelExtensions
{ {
/// <summary> /// <summary>
@@ -40,7 +40,7 @@ public sealed class DialogHueTests
[Fact] [Fact]
public void Compute_DifferentSources_DistributeAcrossPalette() public void Compute_DifferentSources_DistributeAcrossPalette()
{ {
var hues = new[] string[] hues = new[]
{ {
DialogHue.Compute("-100111", "Канал А"), DialogHue.Compute("-100111", "Канал А"),
DialogHue.Compute("-100222", "Канал Б"), DialogHue.Compute("-100222", "Канал Б"),
@@ -2,9 +2,7 @@ using Grpc.Core;
namespace Deal.Telegram.Extensions; namespace Deal.Telegram.Extensions;
/// <summary> // Расширения Exception для классификации сбоев telegram-service.
/// Расширения <see cref="Exception"/> для классификации сбоев telegram-service.
/// </summary>
internal static class ExceptionExtensions internal static class ExceptionExtensions
{ {
/// <summary> /// <summary>