Add customer communication system
This commit is contained in:
165
TeleBot/TeleBot.Tests/Models/OrderFlowTests.cs
Normal file
165
TeleBot/TeleBot.Tests/Models/OrderFlowTests.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using FluentAssertions;
|
||||
using TeleBot.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleBot.Tests.Models
|
||||
{
|
||||
public class OrderFlowTests
|
||||
{
|
||||
[Fact]
|
||||
public void NewOrderFlow_ShouldHaveDefaultValues()
|
||||
{
|
||||
// Arrange & Act
|
||||
var orderFlow = new OrderFlowData();
|
||||
|
||||
// Assert
|
||||
orderFlow.ShippingCountry.Should().Be("United Kingdom");
|
||||
orderFlow.CurrentStep.Should().Be(OrderFlowStep.CollectingName);
|
||||
orderFlow.UsePGPEncryption.Should().BeFalse();
|
||||
orderFlow.IdentityReference.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrderFlow_ShouldProgressThroughSteps()
|
||||
{
|
||||
// Arrange
|
||||
var orderFlow = new OrderFlowData();
|
||||
|
||||
// Act & Assert - Step 1: Name
|
||||
orderFlow.CurrentStep.Should().Be(OrderFlowStep.CollectingName);
|
||||
orderFlow.ShippingName = "John Doe";
|
||||
orderFlow.CurrentStep = OrderFlowStep.CollectingAddress;
|
||||
|
||||
// Step 2: Address
|
||||
orderFlow.CurrentStep.Should().Be(OrderFlowStep.CollectingAddress);
|
||||
orderFlow.ShippingAddress = "123 Main Street";
|
||||
orderFlow.CurrentStep = OrderFlowStep.CollectingCity;
|
||||
|
||||
// Step 3: City
|
||||
orderFlow.CurrentStep.Should().Be(OrderFlowStep.CollectingCity);
|
||||
orderFlow.ShippingCity = "London";
|
||||
orderFlow.CurrentStep = OrderFlowStep.CollectingPostCode;
|
||||
|
||||
// Step 4: PostCode
|
||||
orderFlow.CurrentStep.Should().Be(OrderFlowStep.CollectingPostCode);
|
||||
orderFlow.ShippingPostCode = "SW1A 1AA";
|
||||
orderFlow.CurrentStep = OrderFlowStep.CollectingCountry;
|
||||
|
||||
// Step 5: Country
|
||||
orderFlow.CurrentStep.Should().Be(OrderFlowStep.CollectingCountry);
|
||||
orderFlow.ShippingCountry = "United Kingdom";
|
||||
orderFlow.CurrentStep = OrderFlowStep.ReviewingOrder;
|
||||
|
||||
// Final validation
|
||||
orderFlow.CurrentStep.Should().Be(OrderFlowStep.ReviewingOrder);
|
||||
orderFlow.ShippingName.Should().Be("John Doe");
|
||||
orderFlow.ShippingAddress.Should().Be("123 Main Street");
|
||||
orderFlow.ShippingCity.Should().Be("London");
|
||||
orderFlow.ShippingPostCode.Should().Be("SW1A 1AA");
|
||||
orderFlow.ShippingCountry.Should().Be("United Kingdom");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrderFlow_WithPGPEncryption_ShouldSetFlag()
|
||||
{
|
||||
// Arrange
|
||||
var orderFlow = new OrderFlowData();
|
||||
|
||||
// Act
|
||||
orderFlow.UsePGPEncryption = true;
|
||||
|
||||
// Assert
|
||||
orderFlow.UsePGPEncryption.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrderFlow_WithNotes_ShouldStoreNotes()
|
||||
{
|
||||
// Arrange
|
||||
var orderFlow = new OrderFlowData();
|
||||
|
||||
// Act
|
||||
orderFlow.Notes = "Please leave at the front door";
|
||||
|
||||
// Assert
|
||||
orderFlow.Notes.Should().Be("Please leave at the front door");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(OrderFlowStep.CollectingName)]
|
||||
[InlineData(OrderFlowStep.CollectingAddress)]
|
||||
[InlineData(OrderFlowStep.CollectingCity)]
|
||||
[InlineData(OrderFlowStep.CollectingPostCode)]
|
||||
[InlineData(OrderFlowStep.CollectingCountry)]
|
||||
[InlineData(OrderFlowStep.CollectingNotes)]
|
||||
[InlineData(OrderFlowStep.ReviewingOrder)]
|
||||
[InlineData(OrderFlowStep.SelectingPaymentMethod)]
|
||||
[InlineData(OrderFlowStep.ProcessingPayment)]
|
||||
[InlineData(OrderFlowStep.Complete)]
|
||||
public void OrderFlowStep_AllSteps_ShouldBeDefined(OrderFlowStep step)
|
||||
{
|
||||
// Arrange
|
||||
var orderFlow = new OrderFlowData();
|
||||
|
||||
// Act
|
||||
orderFlow.CurrentStep = step;
|
||||
|
||||
// Assert
|
||||
orderFlow.CurrentStep.Should().Be(step);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrderFlow_SelectedCurrency_ShouldStore()
|
||||
{
|
||||
// Arrange
|
||||
var orderFlow = new OrderFlowData();
|
||||
|
||||
// Act
|
||||
orderFlow.SelectedCurrency = "BTC";
|
||||
|
||||
// Assert
|
||||
orderFlow.SelectedCurrency.Should().Be("BTC");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrderFlow_IdentityReference_ShouldStore()
|
||||
{
|
||||
// Arrange
|
||||
var orderFlow = new OrderFlowData();
|
||||
|
||||
// Act
|
||||
orderFlow.IdentityReference = "ANON-ABC123XYZ";
|
||||
|
||||
// Assert
|
||||
orderFlow.IdentityReference.Should().Be("ANON-ABC123XYZ");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrderFlow_CompleteFlow_ShouldHaveAllRequiredData()
|
||||
{
|
||||
// Arrange
|
||||
var orderFlow = new OrderFlowData
|
||||
{
|
||||
IdentityReference = "ANON-TEST123",
|
||||
ShippingName = "Jane Smith",
|
||||
ShippingAddress = "456 Oak Avenue",
|
||||
ShippingCity = "Manchester",
|
||||
ShippingPostCode = "M1 1AA",
|
||||
ShippingCountry = "United Kingdom",
|
||||
Notes = "Ring doorbell twice",
|
||||
SelectedCurrency = "XMR",
|
||||
UsePGPEncryption = true,
|
||||
CurrentStep = OrderFlowStep.Complete
|
||||
};
|
||||
|
||||
// Assert
|
||||
orderFlow.IdentityReference.Should().NotBeNullOrEmpty();
|
||||
orderFlow.ShippingName.Should().NotBeNullOrEmpty();
|
||||
orderFlow.ShippingAddress.Should().NotBeNullOrEmpty();
|
||||
orderFlow.ShippingCity.Should().NotBeNullOrEmpty();
|
||||
orderFlow.ShippingPostCode.Should().NotBeNullOrEmpty();
|
||||
orderFlow.ShippingCountry.Should().NotBeNullOrEmpty();
|
||||
orderFlow.CurrentStep.Should().Be(OrderFlowStep.Complete);
|
||||
}
|
||||
}
|
||||
}
|
||||
117
TeleBot/TeleBot.Tests/Models/PrivacySettingsTests.cs
Normal file
117
TeleBot/TeleBot.Tests/Models/PrivacySettingsTests.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using FluentAssertions;
|
||||
using TeleBot.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleBot.Tests.Models
|
||||
{
|
||||
public class PrivacySettingsTests
|
||||
{
|
||||
[Fact]
|
||||
public void NewPrivacySettings_ShouldHaveSecureDefaults()
|
||||
{
|
||||
// Arrange & Act
|
||||
var settings = new PrivacySettings();
|
||||
|
||||
// Assert
|
||||
settings.UseEphemeralMode.Should().BeTrue("Ephemeral mode should be enabled by default for privacy");
|
||||
settings.DisableAnalytics.Should().BeTrue("Analytics should be disabled by default for privacy");
|
||||
settings.EnableDisappearingMessages.Should().BeTrue("Disappearing messages should be enabled by default");
|
||||
settings.UseTorOnly.Should().BeFalse("Tor is optional");
|
||||
settings.RequirePGP.Should().BeFalse("PGP is optional");
|
||||
settings.DisappearingMessageTTL.Should().Be(30, "Default TTL should be 30 seconds");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrivacySettings_CanToggleEphemeralMode()
|
||||
{
|
||||
// Arrange
|
||||
var settings = new PrivacySettings();
|
||||
|
||||
// Act
|
||||
settings.UseEphemeralMode = false;
|
||||
|
||||
// Assert
|
||||
settings.UseEphemeralMode.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrivacySettings_CanEnableTor()
|
||||
{
|
||||
// Arrange
|
||||
var settings = new PrivacySettings();
|
||||
|
||||
// Act
|
||||
settings.UseTorOnly = true;
|
||||
|
||||
// Assert
|
||||
settings.UseTorOnly.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrivacySettings_CanSetPGPKey()
|
||||
{
|
||||
// Arrange
|
||||
var settings = new PrivacySettings();
|
||||
var pgpKey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\ntest key\n-----END PGP PUBLIC KEY BLOCK-----";
|
||||
|
||||
// Act
|
||||
settings.RequirePGP = true;
|
||||
settings.PGPPublicKey = pgpKey;
|
||||
|
||||
// Assert
|
||||
settings.RequirePGP.Should().BeTrue();
|
||||
settings.PGPPublicKey.Should().Be(pgpKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrivacySettings_CanDisableAnalytics()
|
||||
{
|
||||
// Arrange
|
||||
var settings = new PrivacySettings();
|
||||
|
||||
// Act
|
||||
settings.DisableAnalytics = false;
|
||||
|
||||
// Assert
|
||||
settings.DisableAnalytics.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrivacySettings_CanSetDisappearingMessageTTL()
|
||||
{
|
||||
// Arrange
|
||||
var settings = new PrivacySettings();
|
||||
|
||||
// Act
|
||||
settings.DisappearingMessageTTL = 60;
|
||||
|
||||
// Assert
|
||||
settings.DisappearingMessageTTL.Should().Be(60);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrivacySettings_MaxPrivacy_Configuration()
|
||||
{
|
||||
// Arrange
|
||||
var settings = new PrivacySettings
|
||||
{
|
||||
UseEphemeralMode = true,
|
||||
UseTorOnly = true,
|
||||
RequirePGP = true,
|
||||
DisableAnalytics = true,
|
||||
EnableDisappearingMessages = true,
|
||||
DisappearingMessageTTL = 10,
|
||||
PGPPublicKey = "test-key"
|
||||
};
|
||||
|
||||
// Assert - All privacy features should be enabled
|
||||
settings.UseEphemeralMode.Should().BeTrue();
|
||||
settings.UseTorOnly.Should().BeTrue();
|
||||
settings.RequirePGP.Should().BeTrue();
|
||||
settings.DisableAnalytics.Should().BeTrue();
|
||||
settings.EnableDisappearingMessages.Should().BeTrue();
|
||||
settings.DisappearingMessageTTL.Should().Be(10);
|
||||
settings.PGPPublicKey.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
234
TeleBot/TeleBot.Tests/Models/ShoppingCartTests.cs
Normal file
234
TeleBot/TeleBot.Tests/Models/ShoppingCartTests.cs
Normal file
@@ -0,0 +1,234 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using TeleBot.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleBot.Tests.Models
|
||||
{
|
||||
public class ShoppingCartTests
|
||||
{
|
||||
[Fact]
|
||||
public void NewCart_ShouldBeEmpty()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cart = new ShoppingCart();
|
||||
|
||||
// Assert
|
||||
cart.IsEmpty().Should().BeTrue();
|
||||
cart.GetTotalItems().Should().Be(0);
|
||||
cart.GetTotalAmount().Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddItem_ShouldAddToCart()
|
||||
{
|
||||
// Arrange
|
||||
var cart = new ShoppingCart();
|
||||
var productId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
cart.AddItem(productId, "Test Product", 10.50m, 2);
|
||||
|
||||
// Assert
|
||||
cart.IsEmpty().Should().BeFalse();
|
||||
cart.GetTotalItems().Should().Be(2);
|
||||
cart.GetTotalAmount().Should().Be(21.00m);
|
||||
cart.Items.Should().HaveCount(1);
|
||||
cart.Items.First().ProductName.Should().Be("Test Product");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddItem_SameProduct_ShouldIncreaseQuantity()
|
||||
{
|
||||
// Arrange
|
||||
var cart = new ShoppingCart();
|
||||
var productId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
cart.AddItem(productId, "Product A", 5.00m, 1);
|
||||
cart.AddItem(productId, "Product A", 5.00m, 2);
|
||||
|
||||
// Assert
|
||||
cart.Items.Should().HaveCount(1);
|
||||
cart.Items.First().Quantity.Should().Be(3);
|
||||
cart.GetTotalAmount().Should().Be(15.00m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddItem_DifferentProducts_ShouldAddSeparately()
|
||||
{
|
||||
// Arrange
|
||||
var cart = new ShoppingCart();
|
||||
var productId1 = Guid.NewGuid();
|
||||
var productId2 = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
cart.AddItem(productId1, "Product A", 10.00m, 1);
|
||||
cart.AddItem(productId2, "Product B", 20.00m, 2);
|
||||
|
||||
// Assert
|
||||
cart.Items.Should().HaveCount(2);
|
||||
cart.GetTotalItems().Should().Be(3);
|
||||
cart.GetTotalAmount().Should().Be(50.00m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveItem_ShouldRemoveFromCart()
|
||||
{
|
||||
// Arrange
|
||||
var cart = new ShoppingCart();
|
||||
var productId = Guid.NewGuid();
|
||||
cart.AddItem(productId, "Test Product", 10.00m, 2);
|
||||
|
||||
// Act
|
||||
cart.RemoveItem(productId);
|
||||
|
||||
// Assert
|
||||
cart.IsEmpty().Should().BeTrue();
|
||||
cart.GetTotalItems().Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveItem_NonExistent_ShouldNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
var cart = new ShoppingCart();
|
||||
var productId = Guid.NewGuid();
|
||||
|
||||
// Act & Assert
|
||||
var act = () => cart.RemoveItem(productId);
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateQuantity_ShouldUpdateExistingItem()
|
||||
{
|
||||
// Arrange
|
||||
var cart = new ShoppingCart();
|
||||
var productId = Guid.NewGuid();
|
||||
cart.AddItem(productId, "Test Product", 10.00m, 2);
|
||||
|
||||
// Act
|
||||
cart.UpdateQuantity(productId, 5);
|
||||
|
||||
// Assert
|
||||
cart.Items.First().Quantity.Should().Be(5);
|
||||
cart.GetTotalAmount().Should().Be(50.00m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateQuantity_ToZero_ShouldRemoveItem()
|
||||
{
|
||||
// Arrange
|
||||
var cart = new ShoppingCart();
|
||||
var productId = Guid.NewGuid();
|
||||
cart.AddItem(productId, "Test Product", 10.00m, 2);
|
||||
|
||||
// Act
|
||||
cart.UpdateQuantity(productId, 0);
|
||||
|
||||
// Assert
|
||||
cart.IsEmpty().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateQuantity_Negative_ShouldRemoveItem()
|
||||
{
|
||||
// Arrange
|
||||
var cart = new ShoppingCart();
|
||||
var productId = Guid.NewGuid();
|
||||
cart.AddItem(productId, "Test Product", 10.00m, 2);
|
||||
|
||||
// Act
|
||||
cart.UpdateQuantity(productId, -1);
|
||||
|
||||
// Assert
|
||||
cart.IsEmpty().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_ShouldEmptyCart()
|
||||
{
|
||||
// Arrange
|
||||
var cart = new ShoppingCart();
|
||||
cart.AddItem(Guid.NewGuid(), "Product A", 10.00m, 2);
|
||||
cart.AddItem(Guid.NewGuid(), "Product B", 20.00m, 1);
|
||||
|
||||
// Act
|
||||
cart.Clear();
|
||||
|
||||
// Assert
|
||||
cart.IsEmpty().Should().BeTrue();
|
||||
cart.GetTotalItems().Should().Be(0);
|
||||
cart.GetTotalAmount().Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CartItem_UpdateTotalPrice_ShouldCalculateCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var item = new CartItem
|
||||
{
|
||||
UnitPrice = 15.50m,
|
||||
Quantity = 3
|
||||
};
|
||||
|
||||
// Act
|
||||
item.UpdateTotalPrice();
|
||||
|
||||
// Assert
|
||||
item.TotalPrice.Should().Be(46.50m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cart_UpdatedAt_ShouldUpdateOnModification()
|
||||
{
|
||||
// Arrange
|
||||
var cart = new ShoppingCart();
|
||||
var originalTime = cart.UpdatedAt;
|
||||
System.Threading.Thread.Sleep(10);
|
||||
|
||||
// Act
|
||||
cart.AddItem(Guid.NewGuid(), "Product", 10.00m, 1);
|
||||
|
||||
// Assert
|
||||
cart.UpdatedAt.Should().BeAfter(originalTime);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(10.99, 1, 10.99)]
|
||||
[InlineData(5.50, 3, 16.50)]
|
||||
[InlineData(0.99, 100, 99.00)]
|
||||
[InlineData(123.45, 2, 246.90)]
|
||||
public void GetTotalAmount_ShouldCalculateCorrectly(decimal price, int quantity, decimal expectedTotal)
|
||||
{
|
||||
// Arrange
|
||||
var cart = new ShoppingCart();
|
||||
|
||||
// Act
|
||||
cart.AddItem(Guid.NewGuid(), "Product", price, quantity);
|
||||
|
||||
// Assert
|
||||
cart.GetTotalAmount().Should().Be(expectedTotal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComplexCart_ShouldCalculateCorrectTotals()
|
||||
{
|
||||
// Arrange
|
||||
var cart = new ShoppingCart();
|
||||
|
||||
// Act
|
||||
cart.AddItem(Guid.NewGuid(), "Laptop", 999.99m, 1);
|
||||
cart.AddItem(Guid.NewGuid(), "Mouse", 25.50m, 2);
|
||||
cart.AddItem(Guid.NewGuid(), "Keyboard", 75.00m, 1);
|
||||
cart.AddItem(Guid.NewGuid(), "Monitor", 350.00m, 2);
|
||||
|
||||
// Assert
|
||||
cart.Items.Should().HaveCount(4);
|
||||
cart.GetTotalItems().Should().Be(6);
|
||||
cart.GetTotalAmount().Should().Be(1825.99m);
|
||||
}
|
||||
}
|
||||
}
|
||||
189
TeleBot/TeleBot.Tests/Services/PrivacyServiceTests.cs
Normal file
189
TeleBot/TeleBot.Tests/Services/PrivacyServiceTests.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using TeleBot.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleBot.Tests.Services
|
||||
{
|
||||
public class PrivacyServiceTests
|
||||
{
|
||||
private readonly PrivacyService _privacyService;
|
||||
private readonly Mock<IConfiguration> _mockConfiguration;
|
||||
private readonly Mock<ILogger<PrivacyService>> _mockLogger;
|
||||
|
||||
public PrivacyServiceTests()
|
||||
{
|
||||
_mockConfiguration = new Mock<IConfiguration>();
|
||||
_mockLogger = new Mock<ILogger<PrivacyService>>();
|
||||
|
||||
// Set up default configuration
|
||||
_mockConfiguration.Setup(x => x["Privacy:HashSalt"])
|
||||
.Returns("TestSalt123");
|
||||
_mockConfiguration.Setup(x => x["Privacy:EnableTor"])
|
||||
.Returns("false");
|
||||
|
||||
_privacyService = new PrivacyService(_mockConfiguration.Object, _mockLogger.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HashIdentifier_ShouldReturnConsistentHash()
|
||||
{
|
||||
// Arrange
|
||||
long telegramId = 123456789;
|
||||
|
||||
// Act
|
||||
var hash1 = _privacyService.HashIdentifier(telegramId);
|
||||
var hash2 = _privacyService.HashIdentifier(telegramId);
|
||||
|
||||
// Assert
|
||||
hash1.Should().NotBeNullOrEmpty();
|
||||
hash2.Should().NotBeNullOrEmpty();
|
||||
hash1.Should().Be(hash2, "Hash should be consistent for the same input");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HashIdentifier_DifferentIds_ShouldReturnDifferentHashes()
|
||||
{
|
||||
// Arrange
|
||||
long telegramId1 = 123456789;
|
||||
long telegramId2 = 987654321;
|
||||
|
||||
// Act
|
||||
var hash1 = _privacyService.HashIdentifier(telegramId1);
|
||||
var hash2 = _privacyService.HashIdentifier(telegramId2);
|
||||
|
||||
// Assert
|
||||
hash1.Should().NotBe(hash2, "Different IDs should produce different hashes");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateAnonymousReference_ShouldReturnUniqueReferences()
|
||||
{
|
||||
// Act
|
||||
var ref1 = _privacyService.GenerateAnonymousReference();
|
||||
var ref2 = _privacyService.GenerateAnonymousReference();
|
||||
|
||||
// Assert
|
||||
ref1.Should().StartWith("ANON-");
|
||||
ref2.Should().StartWith("ANON-");
|
||||
ref1.Should().HaveLength(17); // ANON- (5) + 12 characters
|
||||
ref1.Should().NotBe(ref2, "Each reference should be unique");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateAnonymousReference_ShouldNotContainSpecialCharacters()
|
||||
{
|
||||
// Act
|
||||
var reference = _privacyService.GenerateAnonymousReference();
|
||||
|
||||
// Assert
|
||||
reference.Should().MatchRegex(@"^ANON-[A-Z0-9]+$");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTorHttpClient_WhenTorDisabled_ShouldReturnRegularClient()
|
||||
{
|
||||
// Arrange
|
||||
_mockConfiguration.Setup(x => x["Privacy:EnableTor"])
|
||||
.Returns("false");
|
||||
|
||||
// Act
|
||||
var client = await _privacyService.CreateTorHttpClient();
|
||||
|
||||
// Assert
|
||||
client.Should().NotBeNull();
|
||||
client.Timeout.Should().Be(TimeSpan.FromSeconds(100)); // Default timeout
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncryptData_ShouldEncryptAndDecryptSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var originalData = System.Text.Encoding.UTF8.GetBytes("Secret message");
|
||||
var key = "TestEncryptionKey123";
|
||||
|
||||
// Act
|
||||
var encrypted = _privacyService.EncryptData(originalData, key);
|
||||
var decrypted = _privacyService.DecryptData(encrypted, key);
|
||||
|
||||
// Assert
|
||||
encrypted.Should().NotBeEquivalentTo(originalData, "Data should be encrypted");
|
||||
decrypted.Should().BeEquivalentTo(originalData, "Decrypted data should match original");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncryptData_WithDifferentKeys_ShouldProduceDifferentResults()
|
||||
{
|
||||
// Arrange
|
||||
var originalData = System.Text.Encoding.UTF8.GetBytes("Secret message");
|
||||
var key1 = "Key1";
|
||||
var key2 = "Key2";
|
||||
|
||||
// Act
|
||||
var encrypted1 = _privacyService.EncryptData(originalData, key1);
|
||||
var encrypted2 = _privacyService.EncryptData(originalData, key2);
|
||||
|
||||
// Assert
|
||||
encrypted1.Should().NotBeEquivalentTo(encrypted2, "Different keys should produce different encryptions");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SanitizeLogMessage_ShouldRemoveEmail()
|
||||
{
|
||||
// Arrange
|
||||
var message = "User email is test@example.com in the system";
|
||||
|
||||
// Act
|
||||
_privacyService.SanitizeLogMessage(ref message);
|
||||
|
||||
// Assert
|
||||
message.Should().Be("User email is [EMAIL_REDACTED] in the system");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SanitizeLogMessage_ShouldRemovePhoneNumber()
|
||||
{
|
||||
// Arrange
|
||||
var message = "Contact number: 555-123-4567";
|
||||
|
||||
// Act
|
||||
_privacyService.SanitizeLogMessage(ref message);
|
||||
|
||||
// Assert
|
||||
message.Should().Be("Contact number: [PHONE_REDACTED]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SanitizeLogMessage_ShouldRemoveTelegramId()
|
||||
{
|
||||
// Arrange
|
||||
var message = "Processing request for telegram_id:123456789";
|
||||
|
||||
// Act
|
||||
_privacyService.SanitizeLogMessage(ref message);
|
||||
|
||||
// Assert
|
||||
message.Should().Be("Processing request for telegram_id=[REDACTED]");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("Normal log message without PII")]
|
||||
[InlineData("Order ID: 12345")]
|
||||
public void SanitizeLogMessage_WithoutPII_ShouldRemainUnchanged(string message)
|
||||
{
|
||||
// Arrange
|
||||
var original = message;
|
||||
|
||||
// Act
|
||||
_privacyService.SanitizeLogMessage(ref message);
|
||||
|
||||
// Assert
|
||||
message.Should().Be(original);
|
||||
}
|
||||
}
|
||||
}
|
||||
215
TeleBot/TeleBot.Tests/Services/SessionManagerTests.cs
Normal file
215
TeleBot/TeleBot.Tests/Services/SessionManagerTests.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using TeleBot.Models;
|
||||
using TeleBot.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleBot.Tests.Services
|
||||
{
|
||||
public class SessionManagerTests
|
||||
{
|
||||
private readonly SessionManager _sessionManager;
|
||||
private readonly Mock<IConfiguration> _mockConfiguration;
|
||||
private readonly Mock<ILogger<SessionManager>> _mockLogger;
|
||||
private readonly Mock<IPrivacyService> _mockPrivacyService;
|
||||
private readonly Mock<IDistributedCache> _mockCache;
|
||||
|
||||
public SessionManagerTests()
|
||||
{
|
||||
_mockConfiguration = new Mock<IConfiguration>();
|
||||
_mockLogger = new Mock<ILogger<SessionManager>>();
|
||||
_mockPrivacyService = new Mock<IPrivacyService>();
|
||||
_mockCache = new Mock<IDistributedCache>();
|
||||
|
||||
// Set up default configuration
|
||||
_mockConfiguration.Setup(x => x["Privacy:SessionTimeoutMinutes"])
|
||||
.Returns("30");
|
||||
_mockConfiguration.Setup(x => x["Privacy:EphemeralByDefault"])
|
||||
.Returns("true");
|
||||
_mockConfiguration.Setup(x => x["Redis:Enabled"])
|
||||
.Returns("false");
|
||||
|
||||
// Set up privacy service
|
||||
_mockPrivacyService.Setup(x => x.HashIdentifier(It.IsAny<long>()))
|
||||
.Returns<long>(id => $"HASH_{id}");
|
||||
|
||||
_sessionManager = new SessionManager(
|
||||
_mockConfiguration.Object,
|
||||
_mockLogger.Object,
|
||||
_mockPrivacyService.Object,
|
||||
null // No cache for unit tests
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOrCreateSessionAsync_NewUser_ShouldCreateSession()
|
||||
{
|
||||
// Arrange
|
||||
long telegramUserId = 123456;
|
||||
|
||||
// Act
|
||||
var session = await _sessionManager.GetOrCreateSessionAsync(telegramUserId);
|
||||
|
||||
// Assert
|
||||
session.Should().NotBeNull();
|
||||
session.HashedUserId.Should().Be($"HASH_{telegramUserId}");
|
||||
session.IsEphemeral.Should().BeTrue();
|
||||
session.State.Should().Be(SessionState.MainMenu);
|
||||
session.Cart.Should().NotBeNull();
|
||||
session.Cart.IsEmpty().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOrCreateSessionAsync_ExistingUser_ShouldReturnSameSession()
|
||||
{
|
||||
// Arrange
|
||||
long telegramUserId = 123456;
|
||||
|
||||
// Act
|
||||
var session1 = await _sessionManager.GetOrCreateSessionAsync(telegramUserId);
|
||||
var session2 = await _sessionManager.GetOrCreateSessionAsync(telegramUserId);
|
||||
|
||||
// Assert
|
||||
session1.Id.Should().Be(session2.Id);
|
||||
session1.HashedUserId.Should().Be(session2.HashedUserId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateSessionAsync_ShouldUpdateSession()
|
||||
{
|
||||
// Arrange
|
||||
long telegramUserId = 123456;
|
||||
var session = await _sessionManager.GetOrCreateSessionAsync(telegramUserId);
|
||||
|
||||
// Act
|
||||
session.State = SessionState.BrowsingCategories;
|
||||
session.Cart.AddItem(Guid.NewGuid(), "Test Product", 10.00m, 2);
|
||||
await _sessionManager.UpdateSessionAsync(session);
|
||||
|
||||
var retrievedSession = await _sessionManager.GetOrCreateSessionAsync(telegramUserId);
|
||||
|
||||
// Assert
|
||||
retrievedSession.State.Should().Be(SessionState.BrowsingCategories);
|
||||
retrievedSession.Cart.GetTotalItems().Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteSessionAsync_ShouldRemoveSession()
|
||||
{
|
||||
// Arrange
|
||||
long telegramUserId = 123456;
|
||||
var session = await _sessionManager.GetOrCreateSessionAsync(telegramUserId);
|
||||
var sessionId = session.Id;
|
||||
|
||||
// Act
|
||||
await _sessionManager.DeleteSessionAsync(sessionId);
|
||||
var newSession = await _sessionManager.GetOrCreateSessionAsync(telegramUserId);
|
||||
|
||||
// Assert
|
||||
newSession.Id.Should().NotBe(sessionId, "Should create a new session after deletion");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteUserDataAsync_ShouldRemoveAllUserSessions()
|
||||
{
|
||||
// Arrange
|
||||
long telegramUserId = 123456;
|
||||
var session = await _sessionManager.GetOrCreateSessionAsync(telegramUserId);
|
||||
var originalSessionId = session.Id;
|
||||
|
||||
// Act
|
||||
await _sessionManager.DeleteUserDataAsync(telegramUserId);
|
||||
var newSession = await _sessionManager.GetOrCreateSessionAsync(telegramUserId);
|
||||
|
||||
// Assert
|
||||
newSession.Id.Should().NotBe(originalSessionId);
|
||||
newSession.Cart.IsEmpty().Should().BeTrue();
|
||||
newSession.State.Should().Be(SessionState.MainMenu);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupExpiredSessionsAsync_ShouldRemoveExpiredSessions()
|
||||
{
|
||||
// Arrange
|
||||
long userId1 = 111111;
|
||||
long userId2 = 222222;
|
||||
|
||||
var session1 = await _sessionManager.GetOrCreateSessionAsync(userId1);
|
||||
session1.ExpiresAt = DateTime.UtcNow.AddMinutes(-1); // Expired
|
||||
await _sessionManager.UpdateSessionAsync(session1);
|
||||
|
||||
var session2 = await _sessionManager.GetOrCreateSessionAsync(userId2);
|
||||
// session2 not expired
|
||||
|
||||
// Act
|
||||
await _sessionManager.CleanupExpiredSessionsAsync();
|
||||
|
||||
var retrievedSession1 = await _sessionManager.GetOrCreateSessionAsync(userId1);
|
||||
var retrievedSession2 = await _sessionManager.GetOrCreateSessionAsync(userId2);
|
||||
|
||||
// Assert
|
||||
retrievedSession1.Id.Should().NotBe(session1.Id, "Expired session should be recreated");
|
||||
retrievedSession2.Id.Should().Be(session2.Id, "Non-expired session should remain");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Session_IsExpired_ShouldReturnCorrectStatus()
|
||||
{
|
||||
// Arrange
|
||||
var expiredSession = new UserSession
|
||||
{
|
||||
ExpiresAt = DateTime.UtcNow.AddMinutes(-1)
|
||||
};
|
||||
|
||||
var activeSession = new UserSession
|
||||
{
|
||||
ExpiresAt = DateTime.UtcNow.AddMinutes(30)
|
||||
};
|
||||
|
||||
// Assert
|
||||
expiredSession.IsExpired().Should().BeTrue();
|
||||
activeSession.IsExpired().Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Session_UpdateActivity_ShouldUpdateLastActivityTime()
|
||||
{
|
||||
// Arrange
|
||||
var session = new UserSession();
|
||||
var originalTime = session.LastActivityAt;
|
||||
|
||||
// Act
|
||||
System.Threading.Thread.Sleep(10); // Small delay
|
||||
session.UpdateActivity();
|
||||
|
||||
// Assert
|
||||
session.LastActivityAt.Should().BeAfter(originalTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOrCreateSessionAsync_WithPrivacySettings_ShouldApplyDefaults()
|
||||
{
|
||||
// Arrange
|
||||
_mockConfiguration.Setup(x => x["Privacy:EphemeralByDefault"])
|
||||
.Returns("true");
|
||||
_mockConfiguration.Setup(x => x["Features:EnableDisappearingMessages"])
|
||||
.Returns("true");
|
||||
|
||||
long telegramUserId = 999999;
|
||||
|
||||
// Act
|
||||
var session = await _sessionManager.GetOrCreateSessionAsync(telegramUserId);
|
||||
|
||||
// Assert
|
||||
session.Privacy.Should().NotBeNull();
|
||||
session.Privacy.UseEphemeralMode.Should().BeTrue();
|
||||
session.Privacy.DisableAnalytics.Should().BeTrue();
|
||||
session.Privacy.EnableDisappearingMessages.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
28
TeleBot/TeleBot.Tests/TeleBot.Tests.csproj
Normal file
28
TeleBot/TeleBot.Tests/TeleBot.Tests.csproj
Normal file
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TeleBot\TeleBot.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user