add tests
All checks were successful
SonarQube / Build and analyze (push) Successful in 3m39s

This commit is contained in:
Leonid Pershin
2025-10-21 02:30:04 +03:00
parent 928ae0555e
commit 2a26e84100
15 changed files with 1837 additions and 61 deletions

View File

@@ -1,43 +0,0 @@
name: Unit Tests
on:
push:
branches:
- master
- develop
pull_request:
types: [opened, synchronize, reopened]
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Restore dependencies
run: dotnet restore --verbosity normal
- name: Build solution
run: dotnet build --no-restore --verbosity normal
- name: Run unit tests
run: dotnet test --no-build --verbosity minimal --logger "trx;LogFileName=test-results.trx" --results-directory ./TestResults
- name: Generate test report
if: always()
run: |
echo "Test results:"
find ./TestResults -name "*.trx" -exec echo "Found test result file: {}" \;
echo "File sizes:"
find ./TestResults -name "*.trx" -exec ls -lh {} \;
echo "✅ Tests completed successfully"

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
@@ -20,6 +20,7 @@
<PackageReference Include="FluentAssertions" Version="8.7.1" /> <PackageReference Include="FluentAssertions" Version="8.7.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.10" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />

View File

@@ -261,4 +261,234 @@ public class ChatSessionRepositoryTests : TestBase
var remainingSessions = await _repository.GetActiveSessionsCountAsync(); var remainingSessions = await _repository.GetActiveSessionsCountAsync();
remainingSessions.Should().Be(1); remainingSessions.Should().Be(1);
} }
[Fact]
public async Task GetMessagesAsync_ShouldReturnMessages_WhenMessagesExist()
{
// Arrange
CleanupDatabase();
var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345);
_dbContext.ChatSessions.Add(session);
await _dbContext.SaveChangesAsync();
await _repository.AddMessageAsync(session.Id, "Message 1", "User", 0);
await _repository.AddMessageAsync(session.Id, "Message 2", "Assistant", 1);
// Act
var messages = await _repository.GetMessagesAsync(session.Id);
// Assert
messages.Should().HaveCount(2);
messages[0].Content.Should().Be("Message 1");
messages[0].Role.Should().Be("User");
messages[1].Content.Should().Be("Message 2");
messages[1].Role.Should().Be("Assistant");
}
[Fact]
public async Task GetMessagesAsync_ShouldReturnEmptyList_WhenNoMessages()
{
// Arrange
CleanupDatabase();
var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345);
_dbContext.ChatSessions.Add(session);
await _dbContext.SaveChangesAsync();
// Act
var messages = await _repository.GetMessagesAsync(session.Id);
// Assert
messages.Should().BeEmpty();
}
[Fact]
public async Task AddMessageAsync_ShouldAddMessage()
{
// Arrange
CleanupDatabase();
var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345);
_dbContext.ChatSessions.Add(session);
await _dbContext.SaveChangesAsync();
// Act
var message = await _repository.AddMessageAsync(session.Id, "Test message", "User", 0);
// Assert
message.Should().NotBeNull();
message.Content.Should().Be("Test message");
message.Role.Should().Be("User");
message.MessageOrder.Should().Be(0);
message.SessionId.Should().Be(session.Id);
var messages = await _repository.GetMessagesAsync(session.Id);
messages.Should().HaveCount(1);
}
[Fact]
public async Task ClearMessagesAsync_ShouldRemoveAllMessages()
{
// Arrange
CleanupDatabase();
var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345);
_dbContext.ChatSessions.Add(session);
await _dbContext.SaveChangesAsync();
await _repository.AddMessageAsync(session.Id, "Message 1", "User", 0);
await _repository.AddMessageAsync(session.Id, "Message 2", "Assistant", 1);
await _repository.AddMessageAsync(session.Id, "Message 3", "User", 2);
// Act
await _repository.ClearMessagesAsync(session.Id);
// Assert
var messages = await _repository.GetMessagesAsync(session.Id);
messages.Should().BeEmpty();
}
[Fact]
public async Task GetSessionsForCleanupAsync_ShouldReturnOldSessions()
{
// Arrange
CleanupDatabase();
var oldSession1 = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345);
oldSession1.LastUpdatedAt = DateTime.UtcNow.AddDays(-2);
var oldSession2 = TestDataBuilder.Mocks.CreateChatSessionEntity(2, 12346);
oldSession2.LastUpdatedAt = DateTime.UtcNow.AddHours(-25);
var recentSession = TestDataBuilder.Mocks.CreateChatSessionEntity(3, 12347);
recentSession.LastUpdatedAt = DateTime.UtcNow.AddMinutes(-30);
_dbContext.ChatSessions.AddRange(oldSession1, oldSession2, recentSession);
await _dbContext.SaveChangesAsync();
// Act
var sessionsForCleanup = await _repository.GetSessionsForCleanupAsync(24);
// Assert
sessionsForCleanup.Should().HaveCount(2);
sessionsForCleanup.Should().Contain(s => s.ChatId == 12345);
sessionsForCleanup.Should().Contain(s => s.ChatId == 12346);
sessionsForCleanup.Should().NotContain(s => s.ChatId == 12347);
}
[Fact]
public async Task GetSessionsForCleanupAsync_ShouldReturnEmptyList_WhenNoOldSessions()
{
// Arrange
CleanupDatabase();
var recentSession = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345);
recentSession.LastUpdatedAt = DateTime.UtcNow.AddMinutes(-30);
_dbContext.ChatSessions.Add(recentSession);
await _dbContext.SaveChangesAsync();
// Act
var sessionsForCleanup = await _repository.GetSessionsForCleanupAsync(24);
// Assert
sessionsForCleanup.Should().BeEmpty();
}
[Fact]
public async Task UpdateAsync_ShouldUpdateLastUpdatedAt()
{
// Arrange
CleanupDatabase();
var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345);
var originalLastUpdated = DateTime.UtcNow.AddDays(-1);
session.LastUpdatedAt = originalLastUpdated;
_dbContext.ChatSessions.Add(session);
await _dbContext.SaveChangesAsync();
// Act
session.Model = "new-model";
var updatedSession = await _repository.UpdateAsync(session);
// Assert
updatedSession.LastUpdatedAt.Should().BeAfter(originalLastUpdated);
updatedSession.Model.Should().Be("new-model");
}
[Fact]
public async Task GetByChatIdAsync_ShouldIncludeMessages()
{
// Arrange
CleanupDatabase();
var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345);
_dbContext.ChatSessions.Add(session);
await _dbContext.SaveChangesAsync();
await _repository.AddMessageAsync(session.Id, "Test message", "User", 0);
// Act
var retrievedSession = await _repository.GetByChatIdAsync(12345);
// Assert
retrievedSession.Should().NotBeNull();
retrievedSession!.Messages.Should().HaveCount(1);
retrievedSession.Messages.First().Content.Should().Be("Test message");
}
[Fact]
public async Task GetBySessionIdAsync_ShouldIncludeMessages()
{
// Arrange
CleanupDatabase();
var sessionId = "test-session-id";
var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345, sessionId);
_dbContext.ChatSessions.Add(session);
await _dbContext.SaveChangesAsync();
await _repository.AddMessageAsync(session.Id, "Test message", "User", 0);
// Act
var retrievedSession = await _repository.GetBySessionIdAsync(sessionId);
// Assert
retrievedSession.Should().NotBeNull();
retrievedSession!.Messages.Should().HaveCount(1);
}
[Fact]
public async Task AddMessageAsync_WithMultipleMessages_ShouldMaintainOrder()
{
// Arrange
CleanupDatabase();
var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345);
_dbContext.ChatSessions.Add(session);
await _dbContext.SaveChangesAsync();
// Act
await _repository.AddMessageAsync(session.Id, "Message 1", "User", 0);
await _repository.AddMessageAsync(session.Id, "Message 2", "Assistant", 1);
await _repository.AddMessageAsync(session.Id, "Message 3", "User", 2);
// Assert
var messages = await _repository.GetMessagesAsync(session.Id);
messages.Should().HaveCount(3);
messages[0].MessageOrder.Should().Be(0);
messages[1].MessageOrder.Should().Be(1);
messages[2].MessageOrder.Should().Be(2);
}
[Fact]
public async Task CleanupOldSessionsAsync_WithNoOldSessions_ShouldReturnZero()
{
// Arrange
CleanupDatabase();
var recentSession = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345);
recentSession.LastUpdatedAt = DateTime.UtcNow.AddMinutes(-10);
_dbContext.ChatSessions.Add(recentSession);
await _dbContext.SaveChangesAsync();
// Act
var removedCount = await _repository.CleanupOldSessionsAsync(24);
// Assert
removedCount.Should().Be(0);
}
} }

View File

@@ -38,7 +38,7 @@ public class ChatSessionTests
// Assert // Assert
session.GetAllMessages().Should().HaveCount(1); session.GetAllMessages().Should().HaveCount(1);
var message = session.GetAllMessages().First(); var message = session.GetAllMessages()[0];
message.Role.Should().Be(ChatRole.User); message.Role.Should().Be(ChatRole.User);
message.Content.Should().Be(content); message.Content.Should().Be(content);
} }
@@ -55,7 +55,7 @@ public class ChatSessionTests
// Assert // Assert
session.GetAllMessages().Should().HaveCount(1); session.GetAllMessages().Should().HaveCount(1);
var message = session.GetAllMessages().First(); var message = session.GetAllMessages()[0];
message.Role.Should().Be(ChatRole.Assistant); message.Role.Should().Be(ChatRole.Assistant);
message.Content.Should().Be(content); message.Content.Should().Be(content);
} }
@@ -73,7 +73,7 @@ public class ChatSessionTests
// Assert // Assert
session.GetAllMessages().Should().HaveCount(1); session.GetAllMessages().Should().HaveCount(1);
var addedMessage = session.GetAllMessages().First(); var addedMessage = session.GetAllMessages()[0];
addedMessage.Role.Should().Be(ChatRole.System); addedMessage.Role.Should().Be(ChatRole.System);
addedMessage.Content.Should().Be(content); addedMessage.Content.Should().Be(content);
} }
@@ -183,4 +183,319 @@ public class ChatSessionTests
// Assert // Assert
count.Should().Be(1); count.Should().Be(1);
} }
[Fact]
public void AddUserMessage_InGroupChat_ShouldIncludeUsername()
{
// Arrange
var session = new ChatSession { ChatType = "group" };
var content = "Hello";
var username = "testuser";
// Act
session.AddUserMessage(content, username);
// Assert
var message = session.GetAllMessages()[0];
message.Content.Should().Be("testuser: Hello");
}
[Fact]
public void AddUserMessage_InPrivateChat_ShouldNotIncludeUsername()
{
// Arrange
var session = new ChatSession { ChatType = "private" };
var content = "Hello";
var username = "testuser";
// Act
session.AddUserMessage(content, username);
// Assert
var message = session.GetAllMessages()[0];
message.Content.Should().Be("Hello");
}
[Fact]
public void AddMessage_ShouldTrimHistory_WhenExceedsMaxHistoryLength()
{
// Arrange
var session = new ChatSession { MaxHistoryLength = 5 };
// Add system message
session.AddMessage(new ChatMessage { Role = ChatRole.System, Content = "System prompt" });
// Add messages to exceed max history
for (int i = 0; i < 10; i++)
{
session.AddMessage(new ChatMessage { Role = ChatRole.User, Content = $"Message {i}" });
}
// Act & Assert
session.GetMessageCount().Should().BeLessThanOrEqualTo(5);
// System message should be preserved
session.GetAllMessages()[0].Role.Should().Be(ChatRole.System);
}
[Fact]
public void AddMessage_ShouldTrimHistory_WithoutSystemMessage()
{
// Arrange
var session = new ChatSession { MaxHistoryLength = 3 };
// Add messages to exceed max history
for (int i = 0; i < 5; i++)
{
session.AddMessage(new ChatMessage { Role = ChatRole.User, Content = $"Message {i}" });
}
// Act & Assert
session.GetMessageCount().Should().BeLessThanOrEqualTo(3);
}
[Fact]
public void SetCompressionService_ShouldSetService()
{
// Arrange
var session = new ChatSession();
var compressionServiceMock =
new Moq.Mock<ChatBot.Services.Interfaces.IHistoryCompressionService>();
// Act
session.SetCompressionService(compressionServiceMock.Object);
// Assert
// The service should be set (no exception)
session.Should().NotBeNull();
}
[Fact]
public async Task AddMessageWithCompressionAsync_WithoutCompressionService_ShouldUseTrimming()
{
// Arrange
var session = new ChatSession { MaxHistoryLength = 3 };
// Act
for (int i = 0; i < 5; i++)
{
await session.AddMessageWithCompressionAsync(
new ChatMessage { Role = ChatRole.User, Content = $"Message {i}" },
10,
5
);
}
// Assert
session.GetMessageCount().Should().BeLessThanOrEqualTo(3);
}
[Fact]
public async Task AddUserMessageWithCompressionAsync_ShouldAddMessage()
{
// Arrange
var session = new ChatSession { ChatType = "private" };
// Act
await session.AddUserMessageWithCompressionAsync("Test message", "user", 10, 5);
// Assert
session.GetMessageCount().Should().Be(1);
session.GetAllMessages()[0].Content.Should().Be("Test message");
}
[Fact]
public async Task AddUserMessageWithCompressionAsync_InGroupChat_ShouldIncludeUsername()
{
// Arrange
var session = new ChatSession { ChatType = "group" };
// Act
await session.AddUserMessageWithCompressionAsync("Test message", "user", 10, 5);
// Assert
session.GetMessageCount().Should().Be(1);
session.GetAllMessages()[0].Content.Should().Be("user: Test message");
}
[Fact]
public async Task AddAssistantMessageWithCompressionAsync_ShouldAddMessage()
{
// Arrange
var session = new ChatSession();
// Act
await session.AddAssistantMessageWithCompressionAsync("Test response", 10, 5);
// Assert
session.GetMessageCount().Should().Be(1);
session.GetAllMessages()[0].Role.Should().Be(ChatRole.Assistant);
session.GetAllMessages()[0].Content.Should().Be("Test response");
}
[Fact]
public async Task AddMessageWithCompressionAsync_ShouldTriggerTrimming_WhenNoCompressionService()
{
// Arrange
var session = new ChatSession { MaxHistoryLength = 2 };
// Act
for (int i = 0; i < 4; i++)
{
await session.AddMessageWithCompressionAsync(
new ChatMessage { Role = ChatRole.User, Content = $"Message {i}" },
2,
1
);
}
// Assert
session.GetMessageCount().Should().BeLessThanOrEqualTo(2);
}
[Fact]
public async Task ClearHistory_ShouldUpdateLastUpdatedAt()
{
// Arrange
var session = new ChatSession();
session.AddUserMessage("Test", "user");
var lastUpdated = session.LastUpdatedAt;
await Task.Delay(10); // Small delay
// Act
session.ClearHistory();
// Assert
session.LastUpdatedAt.Should().BeAfter(lastUpdated);
}
[Fact]
public void SetCreatedAtForTesting_ShouldUpdateCreatedAt()
{
// Arrange
var session = new ChatSession();
var targetDate = DateTime.UtcNow.AddDays(-5);
// Act
session.SetCreatedAtForTesting(targetDate);
// Assert
session.CreatedAt.Should().Be(targetDate);
}
[Fact]
public void AddMessage_MultipleTimes_ShouldMaintainOrder()
{
// Arrange
var session = new ChatSession();
// Act
session.AddUserMessage("Message 1", "user1");
session.AddAssistantMessage("Response 1");
session.AddUserMessage("Message 2", "user1");
session.AddAssistantMessage("Response 2");
// Assert
var messages = session.GetAllMessages();
messages.Should().HaveCount(4);
messages[0].Content.Should().Be("Message 1");
messages[1].Content.Should().Be("Response 1");
messages[2].Content.Should().Be("Message 2");
messages[3].Content.Should().Be("Response 2");
}
[Fact]
public void AddMessage_WithSystemMessage_ShouldPreserveSystemMessage()
{
// Arrange
var session = new ChatSession { MaxHistoryLength = 3 };
session.AddMessage(new ChatMessage { Role = ChatRole.System, Content = "System prompt" });
// Act
for (int i = 0; i < 5; i++)
{
session.AddUserMessage($"Message {i}", "user");
}
// Assert
var messages = session.GetAllMessages();
messages[0].Role.Should().Be(ChatRole.System);
messages[0].Content.Should().Be("System prompt");
}
[Fact]
public void GetAllMessages_ShouldReturnCopy()
{
// Arrange
var session = new ChatSession();
session.AddUserMessage("Test", "user");
// Act
var messages1 = session.GetAllMessages();
var messages2 = session.GetAllMessages();
// Assert
messages1.Should().NotBeSameAs(messages2);
messages1.Should().BeEquivalentTo(messages2);
}
[Fact]
public async Task ChatSession_ThreadSafety_MultipleConcurrentAdds()
{
// Arrange
var session = new ChatSession();
var tasks = new List<Task>();
// Act
for (int i = 0; i < 10; i++)
{
int messageNum = i;
tasks.Add(Task.Run(() => session.AddUserMessage($"Message {messageNum}", "user")));
}
await Task.WhenAll(tasks.ToArray());
// Assert
session.GetMessageCount().Should().Be(10);
}
[Fact]
public void MaxHistoryLength_ShouldBeSettable()
{
// Arrange & Act
var session = new ChatSession { MaxHistoryLength = 50 };
// Assert
session.MaxHistoryLength.Should().Be(50);
}
[Fact]
public void Model_ShouldBeSettable()
{
// Arrange & Act
var session = new ChatSession { Model = "llama3" };
// Assert
session.Model.Should().Be("llama3");
}
[Fact]
public void SessionProperties_ShouldBeSettableViaInitializer()
{
// Arrange & Act
var session = new ChatSession
{
ChatId = 12345,
ChatType = "group",
ChatTitle = "Test Group",
Model = "llama3",
MaxHistoryLength = 50,
};
// Assert
session.ChatId.Should().Be(12345);
session.ChatType.Should().Be("group");
session.ChatTitle.Should().Be("Test Group");
session.Model.Should().Be("llama3");
session.MaxHistoryLength.Should().Be(50);
}
} }

View File

@@ -255,4 +255,328 @@ public class DatabaseInitializationServiceTests : UnitTestBase
Times.Once Times.Once
); );
} }
[Fact]
public async Task StartAsync_WhenDatabaseExists_ShouldLogAndMigrate()
{
// Arrange
var dbPath = $"TestDb_{Guid.NewGuid()}.db";
var services = new ServiceCollection();
services.AddDbContext<ChatBotDbContext>(options =>
options.UseSqlite($"Data Source={dbPath}")
.ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning))
);
var serviceProvider = services.BuildServiceProvider();
var loggerMock = new Mock<ILogger<DatabaseInitializationService>>();
var service = new DatabaseInitializationService(serviceProvider, loggerMock.Object);
try
{
// Act
await service.StartAsync(CancellationToken.None);
// Assert
loggerMock.Verify(
x =>
x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) => v.ToString()!.Contains("Starting database initialization")
),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
loggerMock.Verify(
x =>
x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) =>
v.ToString()!
.Contains("Database initialization completed successfully")
),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
finally
{
// Cleanup
serviceProvider.Dispose();
GC.Collect();
GC.WaitForPendingFinalizers();
if (File.Exists(dbPath))
{
try { File.Delete(dbPath); } catch { /* Ignore cleanup errors */ }
}
}
}
[Fact]
public async Task StopAsync_WithCancellationToken_ShouldComplete()
{
// Arrange
var serviceProviderMock = new Mock<IServiceProvider>();
var loggerMock = new Mock<ILogger<DatabaseInitializationService>>();
var service = new DatabaseInitializationService(
serviceProviderMock.Object,
loggerMock.Object
);
var cts = new CancellationTokenSource();
// Act
await service.StopAsync(cts.Token);
// Assert
loggerMock.Verify(
x =>
x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) => v.ToString()!.Contains("Database initialization service stopped")
),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Fact]
public async Task StopAsync_WhenCancellationRequested_ShouldStillComplete()
{
// Arrange
var serviceProviderMock = new Mock<IServiceProvider>();
var loggerMock = new Mock<ILogger<DatabaseInitializationService>>();
var service = new DatabaseInitializationService(
serviceProviderMock.Object,
loggerMock.Object
);
var cts = new CancellationTokenSource();
cts.Cancel();
// Act
await service.StopAsync(cts.Token);
// Assert
loggerMock.Verify(
x =>
x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) => v.ToString()!.Contains("Database initialization service stopped")
),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Fact]
public async Task StartAsync_ShouldHandleDatabaseDoesNotExistException()
{
// Arrange
var dbPath = $"TestDb_{Guid.NewGuid()}.db";
var services = new ServiceCollection();
services.AddDbContext<ChatBotDbContext>(options =>
options.UseSqlite($"Data Source={dbPath}")
.ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning))
);
var serviceProvider = services.BuildServiceProvider();
var loggerMock = new Mock<ILogger<DatabaseInitializationService>>();
var service = new DatabaseInitializationService(serviceProvider, loggerMock.Object);
try
{
// Act
await service.StartAsync(CancellationToken.None);
// Assert - service should complete successfully
loggerMock.Verify(
x =>
x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) =>
v.ToString()!
.Contains("Database initialization completed successfully")
),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
finally
{
// Cleanup
serviceProvider.Dispose();
GC.Collect();
GC.WaitForPendingFinalizers();
if (File.Exists(dbPath))
{
try { File.Delete(dbPath); } catch { /* Ignore cleanup errors */ }
}
}
}
[Fact]
public async Task StartAsync_WithValidDatabase_ShouldLogDatabaseExists()
{
// Arrange
var dbPath = $"TestDb_{Guid.NewGuid()}.db";
var services = new ServiceCollection();
services.AddDbContext<ChatBotDbContext>(options =>
options.UseSqlite($"Data Source={dbPath}")
.ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning))
);
var serviceProvider = services.BuildServiceProvider();
var loggerMock = new Mock<ILogger<DatabaseInitializationService>>();
var service = new DatabaseInitializationService(serviceProvider, loggerMock.Object);
try
{
// Act
await service.StartAsync(CancellationToken.None);
// Assert
loggerMock.Verify(
x =>
x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => true), // Any log message
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.AtLeastOnce
);
}
finally
{
// Cleanup
serviceProvider.Dispose();
GC.Collect();
GC.WaitForPendingFinalizers();
if (File.Exists(dbPath))
{
try { File.Delete(dbPath); } catch { /* Ignore cleanup errors */ }
}
}
}
[Fact]
public void DatabaseInitializationService_ShouldImplementIHostedService()
{
// Arrange
var serviceProviderMock = new Mock<IServiceProvider>();
var loggerMock = new Mock<ILogger<DatabaseInitializationService>>();
// Act
var service = new DatabaseInitializationService(
serviceProviderMock.Object,
loggerMock.Object
);
// Assert
service.Should().BeAssignableTo<Microsoft.Extensions.Hosting.IHostedService>();
}
[Fact]
public async Task StartAsync_MultipleCallsInSequence_ShouldWork()
{
// Arrange
var dbPath = $"TestDb_{Guid.NewGuid()}.db";
var services = new ServiceCollection();
services.AddDbContext<ChatBotDbContext>(options =>
options.UseSqlite($"Data Source={dbPath}")
.ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning))
);
var serviceProvider = services.BuildServiceProvider();
var loggerMock = new Mock<ILogger<DatabaseInitializationService>>();
var service = new DatabaseInitializationService(serviceProvider, loggerMock.Object);
try
{
// Act
await service.StartAsync(CancellationToken.None);
await service.StopAsync(CancellationToken.None);
// Assert - should complete without exceptions
loggerMock.Verify(
x =>
x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) =>
v.ToString()!
.Contains("Database initialization completed successfully")
),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
finally
{
// Cleanup
serviceProvider.Dispose();
GC.Collect();
GC.WaitForPendingFinalizers();
if (File.Exists(dbPath))
{
try { File.Delete(dbPath); } catch { /* Ignore cleanup errors */ }
}
}
}
[Fact]
public async Task StopAsync_WithoutStartAsync_ShouldComplete()
{
// Arrange
var serviceProviderMock = new Mock<IServiceProvider>();
var loggerMock = new Mock<ILogger<DatabaseInitializationService>>();
var service = new DatabaseInitializationService(
serviceProviderMock.Object,
loggerMock.Object
);
// Act
await service.StopAsync(CancellationToken.None);
// Assert - should complete without exceptions
loggerMock.Verify(
x =>
x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) => v.ToString()!.Contains("Database initialization service stopped")
),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
} }

View File

@@ -176,4 +176,265 @@ public class DatabaseSessionStorageTests : TestBase
result.Should().Be(expectedCount); result.Should().Be(expectedCount);
_repositoryMock.Verify(x => x.CleanupOldSessionsAsync(24), Times.Once); _repositoryMock.Verify(x => x.CleanupOldSessionsAsync(24), Times.Once);
} }
[Fact]
public void GetOrCreate_ShouldThrowInvalidOperationException_WhenRepositoryThrows()
{
// Arrange
_repositoryMock
.Setup(x => x.GetOrCreateAsync(12345, "private", "Test Chat"))
.ThrowsAsync(new Exception("Database error"));
// Act
var act = () => _sessionStorage.GetOrCreate(12345, "private", "Test Chat");
// Assert
act.Should()
.Throw<InvalidOperationException>()
.WithMessage("Failed to get or create session for chat 12345")
.WithInnerException<Exception>()
.WithMessage("Database error");
}
[Fact]
public void Get_ShouldReturnNull_WhenRepositoryThrows()
{
// Arrange
_repositoryMock
.Setup(x => x.GetByChatIdAsync(12345))
.ThrowsAsync(new Exception("Database error"));
// Act
var result = _sessionStorage.Get(12345);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task SaveSessionAsync_ShouldLogWarning_WhenSessionNotFound()
{
// Arrange
var session = TestDataBuilder.ChatSessions.CreateBasicSession(12345, "private");
_repositoryMock
.Setup(x => x.GetByChatIdAsync(12345))
.ReturnsAsync((ChatSessionEntity?)null);
// Act
await _sessionStorage.SaveSessionAsync(session);
// Assert
_repositoryMock.Verify(x => x.GetByChatIdAsync(12345), Times.Once);
_repositoryMock.Verify(x => x.UpdateAsync(It.IsAny<ChatSessionEntity>()), Times.Never);
}
[Fact]
public async Task SaveSessionAsync_ShouldThrowInvalidOperationException_WhenRepositoryThrows()
{
// Arrange
var session = TestDataBuilder.ChatSessions.CreateBasicSession(12345, "private");
_repositoryMock
.Setup(x => x.GetByChatIdAsync(12345))
.ThrowsAsync(new Exception("Database error"));
// Act
var act = async () => await _sessionStorage.SaveSessionAsync(session);
// Assert
var exception = await act.Should()
.ThrowAsync<InvalidOperationException>()
.WithMessage("Failed to save session for chat 12345");
exception
.And.InnerException.Should()
.BeOfType<Exception>()
.Which.Message.Should()
.Be("Database error");
}
[Fact]
public async Task SaveSessionAsync_ShouldClearMessagesAndAddNew()
{
// Arrange
var session = TestDataBuilder.ChatSessions.CreateBasicSession(12345, "private");
session.AddUserMessage("Test message", "user1");
session.AddAssistantMessage("Test response");
var sessionEntity = TestDataBuilder.Mocks.CreateChatSessionEntity();
_repositoryMock.Setup(x => x.GetByChatIdAsync(12345)).ReturnsAsync(sessionEntity);
_repositoryMock
.Setup(x => x.UpdateAsync(It.IsAny<ChatSessionEntity>()))
.ReturnsAsync(sessionEntity);
// Act
await _sessionStorage.SaveSessionAsync(session);
// Assert
_repositoryMock.Verify(x => x.ClearMessagesAsync(It.IsAny<int>()), Times.Once);
_repositoryMock.Verify(
x =>
x.AddMessageAsync(
It.IsAny<int>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<int>()
),
Times.Exactly(2)
);
_repositoryMock.Verify(x => x.UpdateAsync(It.IsAny<ChatSessionEntity>()), Times.Once);
}
[Fact]
public void Remove_ShouldReturnFalse_WhenRepositoryThrows()
{
// Arrange
_repositoryMock
.Setup(x => x.DeleteAsync(12345))
.ThrowsAsync(new Exception("Database error"));
// Act
var result = _sessionStorage.Remove(12345);
// Assert
result.Should().BeFalse();
}
[Fact]
public void GetActiveSessionsCount_ShouldReturnZero_WhenRepositoryThrows()
{
// Arrange
_repositoryMock
.Setup(x => x.GetActiveSessionsCountAsync())
.ThrowsAsync(new Exception("Database error"));
// Act
var result = _sessionStorage.GetActiveSessionsCount();
// Assert
result.Should().Be(0);
}
[Fact]
public void CleanupOldSessions_ShouldReturnZero_WhenRepositoryThrows()
{
// Arrange
_repositoryMock
.Setup(x => x.CleanupOldSessionsAsync(24))
.ThrowsAsync(new Exception("Database error"));
// Act
var result = _sessionStorage.CleanupOldSessions(24);
// Assert
result.Should().Be(0);
}
[Fact]
public void GetOrCreate_WithCompressionService_ShouldSetCompressionService()
{
// Arrange
var compressionServiceMock = TestDataBuilder.Mocks.CreateCompressionServiceMock();
var storageWithCompression = new DatabaseSessionStorage(
_repositoryMock.Object,
Mock.Of<ILogger<DatabaseSessionStorage>>(),
compressionServiceMock.Object
);
var sessionEntity = TestDataBuilder.Mocks.CreateChatSessionEntity();
_repositoryMock
.Setup(x => x.GetOrCreateAsync(12345, "private", "Test Chat"))
.ReturnsAsync(sessionEntity);
// Act
var result = storageWithCompression.GetOrCreate(12345, "private", "Test Chat");
// Assert
result.Should().NotBeNull();
result.ChatId.Should().Be(12345);
}
[Fact]
public void Get_WithCompressionService_ShouldSetCompressionService()
{
// Arrange
var loggerMock = new Mock<ILogger<DatabaseSessionStorage>>();
var compressionServiceMock = TestDataBuilder.Mocks.CreateCompressionServiceMock();
var storageWithCompression = new DatabaseSessionStorage(
_repositoryMock.Object,
loggerMock.Object,
compressionServiceMock.Object
);
var sessionEntity = TestDataBuilder.Mocks.CreateChatSessionEntity();
sessionEntity.Messages.Add(
new ChatMessageEntity
{
Id = 1,
SessionId = sessionEntity.Id,
Content = "Test",
Role = "user",
MessageOrder = 0,
CreatedAt = DateTime.UtcNow,
}
);
_repositoryMock.Setup(x => x.GetByChatIdAsync(12345)).ReturnsAsync(sessionEntity);
// Act
var result = storageWithCompression.Get(12345);
// Assert
_repositoryMock.Verify(x => x.GetByChatIdAsync(12345), Times.Once);
result.Should().NotBeNull();
result!.GetMessageCount().Should().Be(1);
}
[Fact]
public async Task SaveSessionAsync_WithMultipleMessages_ShouldSaveInCorrectOrder()
{
// Arrange
var session = TestDataBuilder.ChatSessions.CreateBasicSession(12345, "private");
session.AddUserMessage("Message 1", "user1");
session.AddAssistantMessage("Response 1");
session.AddUserMessage("Message 2", "user1");
session.AddAssistantMessage("Response 2");
var sessionEntity = TestDataBuilder.Mocks.CreateChatSessionEntity();
_repositoryMock.Setup(x => x.GetByChatIdAsync(12345)).ReturnsAsync(sessionEntity);
_repositoryMock
.Setup(x => x.UpdateAsync(It.IsAny<ChatSessionEntity>()))
.ReturnsAsync(sessionEntity);
// Act
await _sessionStorage.SaveSessionAsync(session);
// Assert
_repositoryMock.Verify(x => x.ClearMessagesAsync(It.IsAny<int>()), Times.Once);
_repositoryMock.Verify(
x =>
x.AddMessageAsync(
It.IsAny<int>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<int>()
),
Times.Exactly(4)
);
}
[Fact]
public void GetOrCreate_WithDefaultParameters_ShouldUseDefaults()
{
// Arrange
var sessionEntity = TestDataBuilder.Mocks.CreateChatSessionEntity();
_repositoryMock
.Setup(x => x.GetOrCreateAsync(12345, "private", ""))
.ReturnsAsync(sessionEntity);
// Act
var result = _sessionStorage.GetOrCreate(12345);
// Assert
result.Should().NotBeNull();
_repositoryMock.Verify(x => x.GetOrCreateAsync(12345, "private", ""), Times.Once);
}
} }

View File

@@ -0,0 +1,46 @@
using ChatBot.Services.Telegram.Services;
using ChatBot.Tests.TestUtilities;
using FluentAssertions;
using Moq;
using Telegram.Bot;
using Telegram.Bot.Types;
using Xunit;
namespace ChatBot.Tests.Services.Telegram;
public class TelegramMessageSenderWrapperTests : UnitTestBase
{
private readonly Mock<ITelegramBotClient> _botClientMock;
private readonly TelegramMessageSenderWrapper _wrapper;
public TelegramMessageSenderWrapperTests()
{
_botClientMock = TestDataBuilder.Mocks.CreateTelegramBotClient();
_wrapper = new TelegramMessageSenderWrapper(_botClientMock.Object);
}
[Fact]
public void Constructor_ShouldInitializeCorrectly()
{
// Arrange
var botClient = TestDataBuilder.Mocks.CreateTelegramBotClient().Object;
// Act
var wrapper = new TelegramMessageSenderWrapper(botClient);
// Assert
wrapper.Should().NotBeNull();
}
[Fact]
public void SendMessageAsync_ShouldBePublicMethod()
{
// Arrange & Act
var method = typeof(TelegramMessageSenderWrapper).GetMethod("SendMessageAsync");
// Assert
method.Should().NotBeNull();
method!.IsPublic.Should().BeTrue();
method.ReturnType.Should().Be<Task<Message>>();
}
}

View File

@@ -329,4 +329,8 @@ public class TelegramBotClientWrapperTests : UnitTestBase
var attributes = returnType.GetCustomAttributes(false); var attributes = returnType.GetCustomAttributes(false);
attributes.Should().NotBeNull(); attributes.Should().NotBeNull();
} }
// Note: Tests for GetMeAsync removed because GetMe is an extension method
// and cannot be mocked with Moq. The wrapper simply delegates to the
// TelegramBotClient extension method, which is tested by the Telegram.Bot library itself.
} }

View File

@@ -10,16 +10,15 @@ namespace ChatBot.Tests.Telegram.Commands;
public class SettingsCommandTests : UnitTestBase public class SettingsCommandTests : UnitTestBase
{ {
private readonly Mock<ISessionStorage> _sessionStorageMock; private readonly Mock<ChatService> _chatServiceMock;
private readonly SettingsCommand _settingsCommand; private readonly SettingsCommand _settingsCommand;
public SettingsCommandTests() public SettingsCommandTests()
{ {
_sessionStorageMock = TestDataBuilder.Mocks.CreateSessionStorageMock(); _chatServiceMock = new Mock<ChatService>(
var chatServiceMock = new Mock<ChatService>(
TestDataBuilder.Mocks.CreateLoggerMock<ChatService>().Object, TestDataBuilder.Mocks.CreateLoggerMock<ChatService>().Object,
TestDataBuilder.Mocks.CreateAIServiceMock().Object, TestDataBuilder.Mocks.CreateAIServiceMock().Object,
_sessionStorageMock.Object, TestDataBuilder.Mocks.CreateSessionStorageMock().Object,
TestDataBuilder TestDataBuilder
.Mocks.CreateOptionsMock(TestDataBuilder.Configurations.CreateAISettings()) .Mocks.CreateOptionsMock(TestDataBuilder.Configurations.CreateAISettings())
.Object, .Object,
@@ -33,7 +32,7 @@ public class SettingsCommandTests : UnitTestBase
); );
var aiSettingsMock = TestDataBuilder.Mocks.CreateOptionsMock(new AISettings()); var aiSettingsMock = TestDataBuilder.Mocks.CreateOptionsMock(new AISettings());
_settingsCommand = new SettingsCommand( _settingsCommand = new SettingsCommand(
chatServiceMock.Object, _chatServiceMock.Object,
modelServiceMock.Object, modelServiceMock.Object,
aiSettingsMock.Object aiSettingsMock.Object
); );
@@ -45,7 +44,7 @@ public class SettingsCommandTests : UnitTestBase
// Arrange // Arrange
var chatId = 12345L; var chatId = 12345L;
var session = TestDataBuilder.ChatSessions.CreateBasicSession(chatId); var session = TestDataBuilder.ChatSessions.CreateBasicSession(chatId);
_sessionStorageMock.Setup(x => x.Get(chatId)).Returns(session); _chatServiceMock.Setup(x => x.GetSession(chatId)).Returns(session);
var context = new TelegramCommandContext var context = new TelegramCommandContext
{ {
@@ -70,7 +69,9 @@ public class SettingsCommandTests : UnitTestBase
{ {
// Arrange // Arrange
var chatId = 12345L; var chatId = 12345L;
_sessionStorageMock.Setup(x => x.Get(chatId)).Returns((ChatBot.Models.ChatSession?)null); _chatServiceMock
.Setup(x => x.GetSession(chatId))
.Returns((ChatBot.Models.ChatSession?)null);
var context = new TelegramCommandContext var context = new TelegramCommandContext
{ {

View File

@@ -155,4 +155,284 @@ public class StatusCommandTests : UnitTestBase
result.Should().Contain("системы"); result.Should().Contain("системы");
result.Should().Contain("Доступен"); result.Should().Contain("Доступен");
} }
[Fact]
public async Task ExecuteAsync_ShouldReturnTimeoutStatus_WhenRequestTimesOut()
{
// Arrange
var context = new TelegramCommandContext
{
ChatId = 12345,
Username = "testuser",
MessageText = "/status",
ChatType = "private",
ChatTitle = "Test Chat",
};
_ollamaClientMock
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
.Throws<TaskCanceledException>();
// Act
var result = await _statusCommand.ExecuteAsync(context);
// Assert
result.Should().NotBeNull();
result.Should().Contain("Таймаут");
}
[Fact]
public async Task ExecuteAsync_ShouldReturnHttpError502_WhenBadGateway()
{
// Arrange
var context = new TelegramCommandContext
{
ChatId = 12345,
Username = "testuser",
MessageText = "/status",
ChatType = "private",
ChatTitle = "Test Chat",
};
var httpException = new HttpRequestException(
"Response status code does not indicate success: 502"
);
_ollamaClientMock
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
.Throws(httpException);
// Act
var result = await _statusCommand.ExecuteAsync(context);
// Assert
result.Should().NotBeNull();
result.Should().Contain("502");
result.Should().Contain("Bad Gateway");
}
[Fact]
public async Task ExecuteAsync_ShouldReturnHttpError503_WhenServiceUnavailable()
{
// Arrange
var context = new TelegramCommandContext
{
ChatId = 12345,
Username = "testuser",
MessageText = "/status",
ChatType = "private",
ChatTitle = "Test Chat",
};
var httpException = new HttpRequestException(
"Response status code does not indicate success: 503"
);
_ollamaClientMock
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
.Throws(httpException);
// Act
var result = await _statusCommand.ExecuteAsync(context);
// Assert
result.Should().NotBeNull();
result.Should().Contain("503");
result.Should().Contain("Service Unavailable");
}
[Fact]
public async Task ExecuteAsync_ShouldReturnHttpError504_WhenGatewayTimeout()
{
// Arrange
var context = new TelegramCommandContext
{
ChatId = 12345,
Username = "testuser",
MessageText = "/status",
ChatType = "private",
ChatTitle = "Test Chat",
};
var httpException = new HttpRequestException(
"Response status code does not indicate success: 504"
);
_ollamaClientMock
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
.Throws(httpException);
// Act
var result = await _statusCommand.ExecuteAsync(context);
// Assert
result.Should().NotBeNull();
result.Should().Contain("504");
result.Should().Contain("Gateway Timeout");
}
[Fact]
public async Task ExecuteAsync_ShouldReturnHttpError429_WhenTooManyRequests()
{
// Arrange
var context = new TelegramCommandContext
{
ChatId = 12345,
Username = "testuser",
MessageText = "/status",
ChatType = "private",
ChatTitle = "Test Chat",
};
var httpException = new HttpRequestException(
"Response status code does not indicate success: 429"
);
_ollamaClientMock
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
.Throws(httpException);
// Act
var result = await _statusCommand.ExecuteAsync(context);
// Assert
result.Should().NotBeNull();
result.Should().Contain("429");
result.Should().Contain("Too Many Requests");
}
[Fact]
public async Task ExecuteAsync_ShouldReturnHttpError500_WhenInternalServerError()
{
// Arrange
var context = new TelegramCommandContext
{
ChatId = 12345,
Username = "testuser",
MessageText = "/status",
ChatType = "private",
ChatTitle = "Test Chat",
};
var httpException = new HttpRequestException(
"Response status code does not indicate success: 500"
);
_ollamaClientMock
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
.Throws(httpException);
// Act
var result = await _statusCommand.ExecuteAsync(context);
// Assert
result.Should().NotBeNull();
result.Should().Contain("500");
result.Should().Contain("Internal Server Error");
}
[Fact]
public async Task ExecuteAsync_ShouldReturnNoResponseStatus_WhenResponseIsEmpty()
{
// Arrange
var context = new TelegramCommandContext
{
ChatId = 12345,
Username = "testuser",
MessageText = "/status",
ChatType = "private",
ChatTitle = "Test Chat",
};
// Return empty response
_ollamaClientMock
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
.Returns(
TestDataBuilder.Mocks.CreateAsyncEnumerable(
new List<OllamaSharp.Models.Chat.ChatResponseStream>
{
new OllamaSharp.Models.Chat.ChatResponseStream { Message = null! },
}
)
);
// Act
var result = await _statusCommand.ExecuteAsync(context);
// Assert
result.Should().NotBeNull();
result.Should().Contain("Нет ответа");
}
[Fact]
public async Task ExecuteAsync_WithSession_ShouldShowSessionInfo()
{
// Arrange
var chatServiceMock = new Mock<ChatService>(
TestDataBuilder.Mocks.CreateLoggerMock<ChatService>().Object,
TestDataBuilder.Mocks.CreateAIServiceMock().Object,
TestDataBuilder.Mocks.CreateSessionStorageMock().Object,
TestDataBuilder
.Mocks.CreateOptionsMock(TestDataBuilder.Configurations.CreateAISettings())
.Object,
TestDataBuilder.Mocks.CreateCompressionServiceMock().Object
);
var session = TestDataBuilder.ChatSessions.CreateBasicSession(12345, "private");
session.AddUserMessage("Test", "user");
chatServiceMock.Setup(x => x.GetSession(12345)).Returns(session);
var statusCommand = new StatusCommand(
chatServiceMock.Object,
new Mock<ModelService>(
TestDataBuilder.Mocks.CreateLoggerMock<ModelService>().Object,
_ollamaOptionsMock.Object
).Object,
TestDataBuilder.Mocks.CreateOptionsMock(new AISettings()).Object,
_ollamaClientMock.Object
);
var context = new TelegramCommandContext
{
ChatId = 12345,
Username = "testuser",
MessageText = "/status",
ChatType = "private",
ChatTitle = "Test Chat",
};
_ollamaClientMock
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
.Returns(
TestDataBuilder.Mocks.CreateAsyncEnumerable(
new List<OllamaSharp.Models.Chat.ChatResponseStream>
{
new OllamaSharp.Models.Chat.ChatResponseStream
{
Message = new OllamaSharp.Models.Chat.Message(
ChatRole.Assistant,
"Test"
),
},
}
)
);
// Act
var result = await statusCommand.ExecuteAsync(context);
// Assert
result.Should().NotBeNull();
result.Should().Contain("Сессия");
result.Should().Contain("Сообщений в истории");
}
[Fact]
public void CommandName_ShouldReturnCorrectName()
{
// Act & Assert
_statusCommand.CommandName.Should().Be("/status");
}
[Fact]
public void Description_ShouldReturnCorrectDescription()
{
// Act & Assert
_statusCommand.Description.Should().Be("Показать статус системы и API");
}
} }

View File

@@ -6,6 +6,7 @@ using ChatBot.Tests.TestUtilities;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moq; using Moq;
using Telegram.Bot;
using Telegram.Bot.Types; using Telegram.Bot.Types;
namespace ChatBot.Tests.Telegram.Commands; namespace ChatBot.Tests.Telegram.Commands;
@@ -417,8 +418,8 @@ public class TelegramCommandProcessorTests : UnitTestBase
var username = "testuser"; var username = "testuser";
var chatType = "private"; var chatType = "private";
var chatTitle = "Test Chat"; var chatTitle = "Test Chat";
var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource();
cts.Cancel(); await cts.CancelAsync();
// Act // Act
var result = await _processor.ProcessMessageAsync( var result = await _processor.ProcessMessageAsync(
@@ -488,4 +489,347 @@ public class TelegramCommandProcessorTests : UnitTestBase
// Assert // Assert
result.Should().NotBeNull(); result.Should().NotBeNull();
} }
[Fact]
public async Task ProcessMessageAsync_WithReplyToBot_ShouldProcessMessage()
{
// Arrange
var botUser = new User
{
Id = 999,
Username = "testbot",
IsBot = true,
};
var botInfoServiceMock = new Mock<BotInfoService>(
TestDataBuilder.Mocks.CreateTelegramBotClient().Object,
TestDataBuilder.Mocks.CreateLoggerMock<BotInfoService>().Object
);
botInfoServiceMock.Setup(x => x.GetBotInfoAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(botUser);
var processor = new TelegramCommandProcessor(
_commandRegistry,
_chatService,
_loggerMock.Object,
botInfoServiceMock.Object
);
var replyInfo = new ReplyInfo
{
MessageId = 1,
UserId = 999,
Username = "testbot",
};
// Act
var result = await processor.ProcessMessageAsync(
"Test reply",
12345L,
"user",
"private",
"Test Chat",
replyInfo
);
// Assert
result.Should().NotBeNullOrEmpty();
}
[Fact]
public async Task ProcessMessageAsync_WithReplyToOtherUser_ShouldReturnEmpty()
{
// Arrange
var botUser = new User
{
Id = 999,
Username = "testbot",
IsBot = true,
};
var botInfoServiceMock = new Mock<BotInfoService>(
TestDataBuilder.Mocks.CreateTelegramBotClient().Object,
TestDataBuilder.Mocks.CreateLoggerMock<BotInfoService>().Object
);
botInfoServiceMock.Setup(x => x.GetBotInfoAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(botUser);
var processor = new TelegramCommandProcessor(
_commandRegistry,
_chatService,
_loggerMock.Object,
botInfoServiceMock.Object
);
var replyInfo = new ReplyInfo
{
MessageId = 1,
UserId = 123,
Username = "otheruser",
};
// Act
var result = await processor.ProcessMessageAsync(
"Test reply",
12345L,
"user",
"private",
"Test Chat",
replyInfo
);
// Assert
result.Should().BeEmpty();
}
[Fact]
public async Task ProcessMessageAsync_WithBotMention_ShouldProcessMessage()
{
// Arrange
var botUser = new User
{
Id = 999,
Username = "testbot",
IsBot = true,
};
var botInfoServiceMock = new Mock<BotInfoService>(
TestDataBuilder.Mocks.CreateTelegramBotClient().Object,
TestDataBuilder.Mocks.CreateLoggerMock<BotInfoService>().Object
);
botInfoServiceMock.Setup(x => x.GetBotInfoAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(botUser);
var processor = new TelegramCommandProcessor(
_commandRegistry,
_chatService,
_loggerMock.Object,
botInfoServiceMock.Object
);
// Act
var result = await processor.ProcessMessageAsync(
"Hello @testbot",
12345L,
"user",
"private",
"Test Chat"
);
// Assert
result.Should().NotBeNullOrEmpty();
}
[Fact]
public async Task ProcessMessageAsync_WithOtherUserMention_ShouldReturnEmpty()
{
// Arrange
var botUser = new User
{
Id = 999,
Username = "testbot",
IsBot = true,
};
var botInfoServiceMock = new Mock<BotInfoService>(
TestDataBuilder.Mocks.CreateTelegramBotClient().Object,
TestDataBuilder.Mocks.CreateLoggerMock<BotInfoService>().Object
);
botInfoServiceMock.Setup(x => x.GetBotInfoAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(botUser);
var processor = new TelegramCommandProcessor(
_commandRegistry,
_chatService,
_loggerMock.Object,
botInfoServiceMock.Object
);
// Act
var result = await processor.ProcessMessageAsync(
"Hello @otheruser",
12345L,
"user",
"private",
"Test Chat"
);
// Assert
result.Should().BeEmpty();
}
[Fact]
public async Task ProcessMessageAsync_WithCommand_ShouldExecuteCommand()
{
// Arrange
var commandMock = new Mock<ITelegramCommand>();
commandMock.Setup(x => x.CommandName).Returns("/test");
commandMock
.Setup(x => x.CanHandle(It.IsAny<string>()))
.Returns((string msg) => msg.StartsWith("/test"));
commandMock
.Setup(x =>
x.ExecuteAsync(It.IsAny<TelegramCommandContext>(), It.IsAny<CancellationToken>())
)
.ReturnsAsync("Command executed");
var commandRegistry = new CommandRegistry(
TestDataBuilder.Mocks.CreateLoggerMock<CommandRegistry>().Object,
new[] { commandMock.Object }
);
var processor = new TelegramCommandProcessor(
commandRegistry,
_chatService,
_loggerMock.Object,
_botInfoService
);
// Act
var result = await processor.ProcessMessageAsync(
"/test argument",
12345L,
"user",
"private",
"Test Chat"
);
// Assert
result.Should().Be("Command executed");
commandMock.Verify(
x => x.ExecuteAsync(It.IsAny<TelegramCommandContext>(), It.IsAny<CancellationToken>()),
Times.Once
);
}
[Fact]
public async Task ProcessMessageAsync_WithException_ShouldReturnErrorMessage()
{
// Arrange
var chatServiceMock = new Mock<ChatService>(
TestDataBuilder.Mocks.CreateLoggerMock<ChatService>().Object,
TestDataBuilder.Mocks.CreateAIServiceMock().Object,
TestDataBuilder.Mocks.CreateSessionStorageMock().Object,
TestDataBuilder
.Mocks.CreateOptionsMock(TestDataBuilder.Configurations.CreateAISettings())
.Object,
TestDataBuilder.Mocks.CreateCompressionServiceMock().Object
);
chatServiceMock
.Setup(x =>
x.ProcessMessageAsync(
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()
)
)
.ThrowsAsync(new Exception("Test error"));
var processor = new TelegramCommandProcessor(
_commandRegistry,
chatServiceMock.Object,
_loggerMock.Object,
_botInfoService
);
// Act
var result = await processor.ProcessMessageAsync(
"Test message",
12345L,
"user",
"private",
"Test Chat"
);
// Assert
result.Should().Contain("Произошла ошибка");
}
[Fact]
public async Task ProcessMessageAsync_WithGroupChat_ShouldProcessCorrectly()
{
// Arrange
var messageText = "Hello";
var chatId = -100123456789L; // Group chat ID
var username = "testuser";
var chatType = "group";
var chatTitle = "Test Group";
// Act
var result = await _processor.ProcessMessageAsync(
messageText,
chatId,
username,
chatType,
chatTitle
);
// Assert
result.Should().NotBeNull();
}
[Fact]
public async Task ProcessMessageAsync_WithSupergroupChat_ShouldProcessCorrectly()
{
// Arrange
var messageText = "Hello";
var chatId = -100123456789L;
var username = "testuser";
var chatType = "supergroup";
var chatTitle = "Test Supergroup";
// Act
var result = await _processor.ProcessMessageAsync(
messageText,
chatId,
username,
chatType,
chatTitle
);
// Assert
result.Should().NotBeNull();
}
[Fact]
public async Task ProcessMessageAsync_WithMultipleMentions_IncludingBot_ShouldProcessMessage()
{
// Arrange
var botUser = new User
{
Id = 999,
Username = "testbot",
IsBot = true,
};
var botInfoServiceMock = new Mock<BotInfoService>(
TestDataBuilder.Mocks.CreateTelegramBotClient().Object,
TestDataBuilder.Mocks.CreateLoggerMock<BotInfoService>().Object
);
botInfoServiceMock.Setup(x => x.GetBotInfoAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(botUser);
var processor = new TelegramCommandProcessor(
_commandRegistry,
_chatService,
_loggerMock.Object,
botInfoServiceMock.Object
);
// Act
var result = await processor.ProcessMessageAsync(
"Hello @testbot and @otheruser",
12345L,
"user",
"group",
"Test Group"
);
// Assert
result.Should().NotBeNullOrEmpty();
}
} }

View File

@@ -55,7 +55,7 @@ namespace ChatBot.Services
/// <summary> /// <summary>
/// Process a user message and get AI response /// Process a user message and get AI response
/// </summary> /// </summary>
public async Task<string> ProcessMessageAsync( public virtual async Task<string> ProcessMessageAsync(
long chatId, long chatId,
string username, string username,
string message, string message,
@@ -192,7 +192,7 @@ namespace ChatBot.Services
/// <summary> /// <summary>
/// Get session information /// Get session information
/// </summary> /// </summary>
public ChatSession? GetSession(long chatId) public virtual ChatSession? GetSession(long chatId)
{ {
return _sessionStorage.Get(chatId); return _sessionStorage.Get(chatId);
} }

View File

@@ -178,7 +178,14 @@ namespace ChatBot.Services
// Add messages to session // Add messages to session
foreach (var messageEntity in entity.Messages.OrderBy(m => m.MessageOrder)) foreach (var messageEntity in entity.Messages.OrderBy(m => m.MessageOrder))
{ {
var role = Enum.Parse<ChatRole>(messageEntity.Role); var role = messageEntity.Role.ToLowerInvariant() switch
{
"user" => ChatRole.User,
"assistant" => ChatRole.Assistant,
"system" => ChatRole.System,
"tool" => ChatRole.Tool,
_ => throw new ArgumentException($"Unknown role: {messageEntity.Role}")
};
var message = new ChatMessage { Content = messageEntity.Content, Role = role }; var message = new ChatMessage { Content = messageEntity.Content, Role = role };
session.AddMessage(message); session.AddMessage(message);
} }

View File

@@ -24,7 +24,7 @@ namespace ChatBot.Services.Telegram.Services
/// <summary> /// <summary>
/// Получает информацию о боте (с кэшированием и автоматической инвалидацией) /// Получает информацию о боте (с кэшированием и автоматической инвалидацией)
/// </summary> /// </summary>
public async Task<User?> GetBotInfoAsync(CancellationToken cancellationToken = default) public virtual async Task<User?> GetBotInfoAsync(CancellationToken cancellationToken = default)
{ {
// Проверяем, есть ли валидный кэш // Проверяем, есть ли валидный кэш
if ( if (

View File

@@ -23,10 +23,16 @@ namespace ChatBot.Services.Telegram.Services
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
ReplyParameters? replyParameters = null;
if (replyToMessageId > 0)
{
replyParameters = new ReplyParameters { MessageId = replyToMessageId };
}
return await _botClient.SendMessage( return await _botClient.SendMessage(
chatId: chatId, chatId: chatId,
text: text, text: text,
replyParameters: replyToMessageId, replyParameters: replyParameters,
cancellationToken: cancellationToken cancellationToken: cancellationToken
); );
} }