Add Telegram bot integration and enhance user management features
- Introduced Telegram.Bot package for bot functionality. - Updated user management to include Telegram linking and blocking features. - Enhanced activation request handling with notifications via Telegram. - Added new database entities for Telegram link tokens and login requests. - Implemented traffic synchronization for client stats in the XuiPanelGateway. - Updated application structure to support new test projects and improved dependency injection for Telegram services.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
using PnvPanel.Domain.Activation;
|
||||
using PnvPanel.Domain.Exceptions;
|
||||
using Xunit;
|
||||
|
||||
namespace PnvPanel.Domain.Tests.Activation;
|
||||
|
||||
public class ActivationRequestTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_SetsPendingStatus()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
|
||||
var request = ActivationRequest.Create(userId, "Please activate me");
|
||||
|
||||
Assert.Equal(userId, request.UserId);
|
||||
Assert.Equal("Please activate me", request.Comment);
|
||||
Assert.Equal(ActivationStatus.Pending, request.Status);
|
||||
Assert.Null(request.DecidedBy);
|
||||
Assert.Null(request.DecidedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Approve_WhenPending_SetsApprovedAndDecisionMetadata()
|
||||
{
|
||||
var request = ActivationRequest.Create(Guid.NewGuid(), null);
|
||||
var adminId = Guid.NewGuid();
|
||||
|
||||
request.Approve(adminId);
|
||||
|
||||
Assert.Equal(ActivationStatus.Approved, request.Status);
|
||||
Assert.Equal(adminId, request.DecidedBy);
|
||||
Assert.NotNull(request.DecidedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reject_WhenPending_SetsRejectedWithReason()
|
||||
{
|
||||
var request = ActivationRequest.Create(Guid.NewGuid(), null);
|
||||
var adminId = Guid.NewGuid();
|
||||
|
||||
request.Reject(adminId, "не хватает информации");
|
||||
|
||||
Assert.Equal(ActivationStatus.Rejected, request.Status);
|
||||
Assert.Equal(adminId, request.DecidedBy);
|
||||
Assert.Equal("не хватает информации", request.RejectionReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Approve_WhenAlreadyApproved_Throws()
|
||||
{
|
||||
var request = ActivationRequest.Create(Guid.NewGuid(), null);
|
||||
request.Approve(Guid.NewGuid());
|
||||
|
||||
Assert.Throws<DomainException>(() => request.Approve(Guid.NewGuid()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reject_WhenAlreadyRejected_Throws()
|
||||
{
|
||||
var request = ActivationRequest.Create(Guid.NewGuid(), null);
|
||||
request.Reject(Guid.NewGuid(), null);
|
||||
|
||||
Assert.Throws<DomainException>(() => request.Reject(Guid.NewGuid(), null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reject_WhenAlreadyApproved_Throws()
|
||||
{
|
||||
var request = ActivationRequest.Create(Guid.NewGuid(), null);
|
||||
request.Approve(Guid.NewGuid());
|
||||
|
||||
Assert.Throws<DomainException>(() => request.Reject(Guid.NewGuid(), null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using PnvPanel.Domain.Apps;
|
||||
using Xunit;
|
||||
|
||||
namespace PnvPanel.Domain.Tests.Apps;
|
||||
|
||||
public class ClientAppTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_SetsEnabledByDefault()
|
||||
{
|
||||
var app = ClientApp.Create("v2rayNG", new Uri("https://play.google.com/store/apps/details?id=x"), OsPlatform.Android, "desc", null, 1);
|
||||
|
||||
Assert.Equal("v2rayNG", app.Name);
|
||||
Assert.Equal(OsPlatform.Android, app.OperatingSystem);
|
||||
Assert.True(app.IsEnabled);
|
||||
Assert.Equal(1, app.SortOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReplacesAllMutableFields()
|
||||
{
|
||||
var app = ClientApp.Create("Old", new Uri("https://old.example.com"), OsPlatform.IOS, "old", "old-icon", 1);
|
||||
|
||||
app.Update("New", new Uri("https://new.example.com"), OsPlatform.MacOS, "new", "new-icon", 2, isEnabled: false);
|
||||
|
||||
Assert.Equal("New", app.Name);
|
||||
Assert.Equal(new Uri("https://new.example.com"), app.DownloadUrl);
|
||||
Assert.Equal(OsPlatform.MacOS, app.OperatingSystem);
|
||||
Assert.Equal("new", app.Description);
|
||||
Assert.Equal("new-icon", app.IconUrl);
|
||||
Assert.Equal(2, app.SortOrder);
|
||||
Assert.False(app.IsEnabled);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using PnvPanel.Domain.Audit;
|
||||
using Xunit;
|
||||
|
||||
namespace PnvPanel.Domain.Tests.Audit;
|
||||
|
||||
public class AuditLogTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_SetsAllFieldsAndCreatedAt()
|
||||
{
|
||||
var actorId = Guid.NewGuid();
|
||||
|
||||
var log = AuditLog.Create(actorId, "user.blocked", "AppUser", actorId.ToString(), "{\"reason\":\"abuse\"}", AuditSource.Web);
|
||||
|
||||
Assert.Equal(actorId, log.ActorId);
|
||||
Assert.Equal("user.blocked", log.Action);
|
||||
Assert.Equal("AppUser", log.TargetType);
|
||||
Assert.Equal(actorId.ToString(), log.TargetId);
|
||||
Assert.Equal("{\"reason\":\"abuse\"}", log.Metadata);
|
||||
Assert.Equal(AuditSource.Web, log.Source);
|
||||
Assert.True(log.CreatedAt <= DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_AllowsNullActorForSystemActions()
|
||||
{
|
||||
var log = AuditLog.Create(null, "node.healthcheck", "Node", Guid.NewGuid().ToString(), null, AuditSource.System);
|
||||
|
||||
Assert.Null(log.ActorId);
|
||||
Assert.Equal(AuditSource.System, log.Source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using PnvPanel.Domain.Common;
|
||||
using Xunit;
|
||||
|
||||
namespace PnvPanel.Domain.Tests.Common;
|
||||
|
||||
public class EntityTests
|
||||
{
|
||||
private sealed class FakeEntityA : Entity
|
||||
{
|
||||
public FakeEntityA(Guid id) => Id = id;
|
||||
}
|
||||
|
||||
private sealed class FakeEntityB : Entity
|
||||
{
|
||||
public FakeEntityB(Guid id) => Id = id;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_SameTypeAndId_ReturnsTrue()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var a = new FakeEntityA(id);
|
||||
var b = new FakeEntityA(id);
|
||||
|
||||
Assert.Equal(a, b);
|
||||
Assert.True(a == b);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_DifferentTypesSameId_ReturnsFalse()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var a = new FakeEntityA(id);
|
||||
var b = new FakeEntityB(id);
|
||||
|
||||
Assert.False(a.Equals(b));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_SameTypeDifferentId_ReturnsFalse()
|
||||
{
|
||||
var a = new FakeEntityA(Guid.NewGuid());
|
||||
var b = new FakeEntityA(Guid.NewGuid());
|
||||
|
||||
Assert.NotEqual(a, b);
|
||||
Assert.True(a != b);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_SameTypeAndId_AreEqual()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var a = new FakeEntityA(id);
|
||||
var b = new FakeEntityA(id);
|
||||
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using PnvPanel.Domain.Configs;
|
||||
using Xunit;
|
||||
|
||||
namespace PnvPanel.Domain.Tests.Configs;
|
||||
|
||||
public class TrafficSampleTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_SetsAllFields()
|
||||
{
|
||||
var configId = Guid.NewGuid();
|
||||
var timestamp = DateTimeOffset.UtcNow;
|
||||
|
||||
var sample = TrafficSample.Create(configId, timestamp, upBytes: 1000, downBytes: 2000);
|
||||
|
||||
Assert.Equal(configId, sample.ConfigId);
|
||||
Assert.Equal(timestamp, sample.Timestamp);
|
||||
Assert.Equal(1000, sample.UpBytes);
|
||||
Assert.Equal(2000, sample.DownBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
using PnvPanel.Domain.Configs;
|
||||
using PnvPanel.Domain.Exceptions;
|
||||
using PnvPanel.Domain.Inbounds;
|
||||
using Xunit;
|
||||
|
||||
namespace PnvPanel.Domain.Tests.Configs;
|
||||
|
||||
public class VpnConfigTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_SetsActiveStatusAndGeneratesEmailAndToken()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var inboundId = Guid.NewGuid();
|
||||
|
||||
var config = VpnConfig.Create(userId, inboundId, VpnProtocol.Vless, "My device", deviceLimit: 3);
|
||||
|
||||
Assert.Equal(userId, config.UserId);
|
||||
Assert.Equal(inboundId, config.InboundId);
|
||||
Assert.Equal(VpnProtocol.Vless, config.Protocol);
|
||||
Assert.Equal("My device", config.Label);
|
||||
Assert.Equal(3, config.DeviceLimit);
|
||||
Assert.Equal(ConfigStatus.Active, config.Status);
|
||||
Assert.Equal(string.Empty, config.ClientExternalId);
|
||||
Assert.False(string.IsNullOrWhiteSpace(config.ClientEmail));
|
||||
Assert.StartsWith("pnv_", config.ClientEmail);
|
||||
Assert.False(string.IsNullOrWhiteSpace(config.SubscriptionToken));
|
||||
Assert.NotEqual(Guid.Empty, config.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_GeneratesUniqueSubscriptionTokensAndClientEmails()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var a = VpnConfig.Create(userId, Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
var b = VpnConfig.Create(userId, Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
|
||||
Assert.NotEqual(a.SubscriptionToken, b.SubscriptionToken);
|
||||
Assert.NotEqual(a.ClientEmail, b.ClientEmail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssignRemoteClient_SetsClientExternalId()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Trojan, null, 1);
|
||||
|
||||
config.AssignRemoteClient("some-remote-password");
|
||||
|
||||
Assert.Equal("some-remote-password", config.ClientExternalId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rotate_WhenActive_ChangesEmailExternalIdAndToken()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
config.AssignRemoteClient("old-id");
|
||||
var oldToken = config.SubscriptionToken;
|
||||
var oldEmail = config.ClientEmail;
|
||||
|
||||
config.Rotate("new-email", "new-id");
|
||||
|
||||
Assert.Equal("new-email", config.ClientEmail);
|
||||
Assert.Equal("new-id", config.ClientExternalId);
|
||||
Assert.NotEqual(oldToken, config.SubscriptionToken);
|
||||
Assert.NotEqual(oldEmail, config.ClientEmail);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ConfigStatus.Revoked)]
|
||||
[InlineData(ConfigStatus.Disabled)]
|
||||
public void Rotate_WhenNotActive_Throws(ConfigStatus status)
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
MoveToStatus(config, status);
|
||||
|
||||
Assert.Throws<DomainException>(() => config.Rotate("e", "i"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Revoke_WhenActive_SetsRevokedStatus()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
|
||||
config.Revoke();
|
||||
|
||||
Assert.Equal(ConfigStatus.Revoked, config.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Revoke_WhenAlreadyRevoked_Throws()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
config.Revoke();
|
||||
|
||||
Assert.Throws<DomainException>(() => config.Revoke());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Disable_WhenActive_SetsDisabled()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
|
||||
config.Disable();
|
||||
|
||||
Assert.Equal(ConfigStatus.Disabled, config.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Disable_WhenRevoked_DoesNotChangeStatus()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
config.Revoke();
|
||||
|
||||
config.Disable();
|
||||
|
||||
Assert.Equal(ConfigStatus.Revoked, config.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enable_WhenDisabled_ReturnsToActive()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
config.Disable();
|
||||
|
||||
config.Enable();
|
||||
|
||||
Assert.Equal(ConfigStatus.Active, config.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enable_WhenRevoked_DoesNotResurrect()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
config.Revoke();
|
||||
|
||||
config.Enable();
|
||||
|
||||
Assert.Equal(ConfigStatus.Revoked, config.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateTraffic_SetsBytesAndLastSyncAt()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
|
||||
config.UpdateTraffic(100, 200);
|
||||
|
||||
Assert.Equal(100, config.UsedUpBytes);
|
||||
Assert.Equal(200, config.UsedDownBytes);
|
||||
Assert.NotNull(config.LastSyncAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientEmail_IsDeterministicPrefixWithRandomSuffix()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var email1 = VpnConfig.GenerateClientEmail(userId);
|
||||
var email2 = VpnConfig.GenerateClientEmail(userId);
|
||||
|
||||
var expectedPrefix = $"pnv_{userId:N}"[..12];
|
||||
Assert.StartsWith(expectedPrefix, email1);
|
||||
Assert.NotEqual(email1, email2);
|
||||
}
|
||||
|
||||
private static void MoveToStatus(VpnConfig config, ConfigStatus status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case ConfigStatus.Revoked:
|
||||
config.Revoke();
|
||||
break;
|
||||
case ConfigStatus.Disabled:
|
||||
config.Disable();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using PnvPanel.Domain.Inbounds;
|
||||
using Xunit;
|
||||
|
||||
namespace PnvPanel.Domain.Tests.Inbounds;
|
||||
|
||||
public class InboundTests
|
||||
{
|
||||
[Fact]
|
||||
public void FromRemote_CreatesUnpublishedInbound()
|
||||
{
|
||||
var nodeId = Guid.NewGuid();
|
||||
|
||||
var inbound = Inbound.FromRemote(nodeId, "12", VpnProtocol.Vless, "Germany", 443);
|
||||
|
||||
Assert.Equal(nodeId, inbound.NodeId);
|
||||
Assert.Equal("12", inbound.RemoteInboundId);
|
||||
Assert.Equal(VpnProtocol.Vless, inbound.Protocol);
|
||||
Assert.Equal(443, inbound.Port);
|
||||
Assert.False(inbound.IsPublished);
|
||||
Assert.Empty(inbound.AllowedRoleIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateFromRemote_UpdatesFieldsAndLastSyncAt()
|
||||
{
|
||||
var inbound = Inbound.FromRemote(Guid.NewGuid(), "12", VpnProtocol.Vless, "Old", 443);
|
||||
var before = inbound.LastSyncAt;
|
||||
|
||||
inbound.UpdateFromRemote(VpnProtocol.Trojan, "New", 8443);
|
||||
|
||||
Assert.Equal(VpnProtocol.Trojan, inbound.Protocol);
|
||||
Assert.Equal("New", inbound.Remark);
|
||||
Assert.Equal(8443, inbound.Port);
|
||||
Assert.NotNull(inbound.LastSyncAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Publish_SetsDisplayNameRolesAndMaxClients()
|
||||
{
|
||||
var inbound = Inbound.FromRemote(Guid.NewGuid(), "12", VpnProtocol.Vless, "Germany", 443);
|
||||
var roleId = Guid.NewGuid();
|
||||
|
||||
inbound.Publish("Germany (VLESS)", [roleId, roleId], 100);
|
||||
|
||||
Assert.True(inbound.IsPublished);
|
||||
Assert.Equal("Germany (VLESS)", inbound.DisplayName);
|
||||
Assert.Equal(100, inbound.MaxClients);
|
||||
Assert.Single(inbound.AllowedRoleIds);
|
||||
Assert.Contains(roleId, inbound.AllowedRoleIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unpublish_SetsIsPublishedFalseButKeepsRoles()
|
||||
{
|
||||
var inbound = Inbound.FromRemote(Guid.NewGuid(), "12", VpnProtocol.Vless, "Germany", 443);
|
||||
var roleId = Guid.NewGuid();
|
||||
inbound.Publish("Germany", [roleId], null);
|
||||
|
||||
inbound.Unpublish();
|
||||
|
||||
Assert.False(inbound.IsPublished);
|
||||
Assert.Contains(roleId, inbound.AllowedRoleIds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using PnvPanel.Domain.Exceptions;
|
||||
using PnvPanel.Domain.Nodes;
|
||||
using Xunit;
|
||||
|
||||
namespace PnvPanel.Domain.Tests.Nodes;
|
||||
|
||||
public class NodeTests
|
||||
{
|
||||
private static NodeCredentials Credentials => new("admin", "protected-secret");
|
||||
|
||||
[Fact]
|
||||
public void Register_WithAbsoluteUri_CreatesEnabledUnknownStatusNode()
|
||||
{
|
||||
var node = Node.Register("Germany-1", new Uri("https://de1.example.com:2053"), Credentials, "Germany");
|
||||
|
||||
Assert.Equal("Germany-1", node.Name);
|
||||
Assert.Equal("Germany", node.Location);
|
||||
Assert.Equal(NodeStatus.Unknown, node.Status);
|
||||
Assert.True(node.IsEnabled);
|
||||
Assert.NotEqual(Guid.Empty, node.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_WithRelativeUri_Throws()
|
||||
{
|
||||
var relativeUri = new Uri("de1.example.com", UriKind.Relative);
|
||||
|
||||
Assert.Throws<DomainException>(() => Node.Register("Germany-1", relativeUri, Credentials, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateDetails_ChangesNameAndLocation()
|
||||
{
|
||||
var node = Node.Register("Old", new Uri("https://example.com"), Credentials, "Old location");
|
||||
|
||||
node.UpdateDetails("New", "New location");
|
||||
|
||||
Assert.Equal("New", node.Name);
|
||||
Assert.Equal("New location", node.Location);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateCredentials_ReplacesCredentials()
|
||||
{
|
||||
var node = Node.Register("Node", new Uri("https://example.com"), Credentials, null);
|
||||
var newCredentials = new NodeCredentials("root", "new-protected-secret");
|
||||
|
||||
node.UpdateCredentials(newCredentials);
|
||||
|
||||
Assert.Equal(newCredentials, node.Credentials);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Disable_ThenEnable_TogglesIsEnabled()
|
||||
{
|
||||
var node = Node.Register("Node", new Uri("https://example.com"), Credentials, null);
|
||||
|
||||
node.Disable();
|
||||
Assert.False(node.IsEnabled);
|
||||
|
||||
node.Enable();
|
||||
Assert.True(node.IsEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateStatus_SetsStatus()
|
||||
{
|
||||
var node = Node.Register("Node", new Uri("https://example.com"), Credentials, null);
|
||||
|
||||
node.UpdateStatus(NodeStatus.Online);
|
||||
|
||||
Assert.Equal(NodeStatus.Online, node.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MarkSynced_SetsLastSyncAt()
|
||||
{
|
||||
var node = Node.Register("Node", new Uri("https://example.com"), Credentials, null);
|
||||
Assert.Null(node.LastSyncAt);
|
||||
|
||||
node.MarkSynced();
|
||||
|
||||
Assert.NotNull(node.LastSyncAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NodeCredentials_ToString_RedactsPassword()
|
||||
{
|
||||
var text = Credentials.ToString();
|
||||
|
||||
Assert.DoesNotContain("protected-secret", text);
|
||||
Assert.Contains("REDACTED", text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<!-- Тестовый код: имена вида Method_Scenario_Result и т.п. не обязаны следовать
|
||||
анализаторам, рассчитанным на публичный production-код. -->
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\PnvPanel.Domain\PnvPanel.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,59 @@
|
||||
using PnvPanel.Domain.Exceptions;
|
||||
using PnvPanel.Domain.Telegram;
|
||||
using Xunit;
|
||||
|
||||
namespace PnvPanel.Domain.Tests.Telegram;
|
||||
|
||||
public class TelegramLinkTokenTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_IsValidBeforeConsumptionOrExpiry()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
|
||||
var token = TelegramLinkToken.Create(userId, TimeSpan.FromMinutes(10));
|
||||
|
||||
Assert.Equal(userId, token.UserId);
|
||||
Assert.True(token.IsValid);
|
||||
Assert.Null(token.ConsumedAt);
|
||||
Assert.False(string.IsNullOrWhiteSpace(token.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_GeneratesUniqueTokens()
|
||||
{
|
||||
var a = TelegramLinkToken.Create(Guid.NewGuid(), TimeSpan.FromMinutes(10));
|
||||
var b = TelegramLinkToken.Create(Guid.NewGuid(), TimeSpan.FromMinutes(10));
|
||||
|
||||
Assert.NotEqual(a.Token, b.Token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Consume_WhenValid_SetsConsumedAtAndInvalidates()
|
||||
{
|
||||
var token = TelegramLinkToken.Create(Guid.NewGuid(), TimeSpan.FromMinutes(10));
|
||||
|
||||
token.Consume();
|
||||
|
||||
Assert.NotNull(token.ConsumedAt);
|
||||
Assert.False(token.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Consume_WhenAlreadyConsumed_Throws()
|
||||
{
|
||||
var token = TelegramLinkToken.Create(Guid.NewGuid(), TimeSpan.FromMinutes(10));
|
||||
token.Consume();
|
||||
|
||||
Assert.Throws<DomainException>(() => token.Consume());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Consume_WhenExpired_Throws()
|
||||
{
|
||||
var token = TelegramLinkToken.Create(Guid.NewGuid(), TimeSpan.Zero);
|
||||
|
||||
Assert.False(token.IsValid);
|
||||
Assert.Throws<DomainException>(() => token.Consume());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using PnvPanel.Domain.Exceptions;
|
||||
using PnvPanel.Domain.Telegram;
|
||||
using Xunit;
|
||||
|
||||
namespace PnvPanel.Domain.Tests.Telegram;
|
||||
|
||||
public class TelegramLoginRequestTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_SetsPendingStatusAndExpiry()
|
||||
{
|
||||
var request = TelegramLoginRequest.Create(TimeSpan.FromMinutes(5), "1.2.3.4");
|
||||
|
||||
Assert.Equal(TelegramLoginStatus.Pending, request.Status);
|
||||
Assert.Equal("1.2.3.4", request.Context);
|
||||
Assert.False(request.IsExpired);
|
||||
Assert.True(request.ExpiresAt > request.CreatedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Approve_WhenPending_SetsApprovedAndUserId()
|
||||
{
|
||||
var request = TelegramLoginRequest.Create(TimeSpan.FromMinutes(5), null);
|
||||
var userId = Guid.NewGuid();
|
||||
|
||||
request.Approve(userId);
|
||||
|
||||
Assert.Equal(TelegramLoginStatus.Approved, request.Status);
|
||||
Assert.Equal(userId, request.UserId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reject_WhenPending_SetsRejected()
|
||||
{
|
||||
var request = TelegramLoginRequest.Create(TimeSpan.FromMinutes(5), null);
|
||||
|
||||
request.Reject();
|
||||
|
||||
Assert.Equal(TelegramLoginStatus.Rejected, request.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Approve_WhenExpired_ThrowsAndMarksExpired()
|
||||
{
|
||||
var request = TelegramLoginRequest.Create(TimeSpan.Zero, null);
|
||||
|
||||
Assert.Throws<DomainException>(() => request.Approve(Guid.NewGuid()));
|
||||
Assert.Equal(TelegramLoginStatus.Expired, request.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Approve_WhenAlreadyApproved_Throws()
|
||||
{
|
||||
var request = TelegramLoginRequest.Create(TimeSpan.FromMinutes(5), null);
|
||||
request.Approve(Guid.NewGuid());
|
||||
|
||||
Assert.Throws<DomainException>(() => request.Approve(Guid.NewGuid()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Consume_WhenApproved_SetsConsumed()
|
||||
{
|
||||
var request = TelegramLoginRequest.Create(TimeSpan.FromMinutes(5), null);
|
||||
request.Approve(Guid.NewGuid());
|
||||
|
||||
request.Consume();
|
||||
|
||||
Assert.Equal(TelegramLoginStatus.Consumed, request.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Consume_WhenNotApproved_Throws()
|
||||
{
|
||||
var request = TelegramLoginRequest.Create(TimeSpan.FromMinutes(5), null);
|
||||
|
||||
Assert.Throws<DomainException>(() => request.Consume());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Consume_WhenAlreadyConsumed_Throws()
|
||||
{
|
||||
var request = TelegramLoginRequest.Create(TimeSpan.FromMinutes(5), null);
|
||||
request.Approve(Guid.NewGuid());
|
||||
request.Consume();
|
||||
|
||||
Assert.Throws<DomainException>(() => request.Consume());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user