From 40f294f61bf5d45f8d06a8dcbbace36a64b0cddf Mon Sep 17 00:00:00 2001 From: stasex Date: Wed, 25 Oct 2023 17:28:55 +0300 Subject: [PATCH] added new tests --- .../Models/OpenAi/ChatCompletionRequest.cs | 2 +- .../Services/ProductService.cs | 60 ++-- .../Tests/ProductsTests.cs | 4 +- .../ProductTests.cs | 280 +++++++++++------- 4 files changed, 213 insertions(+), 133 deletions(-) diff --git a/ShoppingAssistantApi.Application/Models/OpenAi/ChatCompletionRequest.cs b/ShoppingAssistantApi.Application/Models/OpenAi/ChatCompletionRequest.cs index d2ad66b..40f3972 100644 --- a/ShoppingAssistantApi.Application/Models/OpenAi/ChatCompletionRequest.cs +++ b/ShoppingAssistantApi.Application/Models/OpenAi/ChatCompletionRequest.cs @@ -2,7 +2,7 @@ namespace ShoppingAssistantApi.Application.Models.OpenAi; public class ChatCompletionRequest { - public string Model { get; set; } = "gpt-3.5-turbo"; + public string Model { get; set; } = "gpt-4"; public List Messages { get; set; } diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs index 5d0ff84..d86bae6 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs @@ -29,6 +29,14 @@ public class ProductService : IProductService public async IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken) { + string promptForGpt = + "You are a Shopping Assistant that helps people find product recommendations. Ask user additional questions if more context needed." + + "\nYou must return data with one of the prefixes:" + + "\n[Question] - return question" + + "\n[Suggestions] - return semicolon separated suggestion how to answer to a question" + + "\n[Message] - return text" + + "\n[Products] - return semicolon separated product names"; + var isFirstMessage = await _messagesRepository.GetCountAsync(message=>message.WishlistId==ObjectId.Parse((wishlistId)), cancellationToken); var chatRequest = new ChatCompletionRequest(); @@ -42,12 +50,7 @@ public class ProductService : IProductService new OpenAiMessage { Role = OpenAiRole.System.ToString().ToLower(), - Content = "You are a Shopping Assistant that helps people find product recommendations. Ask user additional questions if more context needed." + - "\nYou must return data with one of the prefixes:" + - "\n[Question] - return question" + - "\n[Suggestions] - return semicolon separated suggestion how to answer to a question" + - "\n[Message] - return text" + - "\n[Products] - return semicolon separated product names" + Content = promptForGpt }, new OpenAiMessage() @@ -70,17 +73,6 @@ public class ProductService : IProductService Data = "What are you looking for?" }; - yield return new ServerSentEvent - { - Event = SearchEventType.Suggestion, - Data = "Bicycle" - }; - - yield return new ServerSentEvent - { - Event = SearchEventType.Suggestion, - Data = "Laptop" - }; } if(isFirstMessage!=0) @@ -89,6 +81,11 @@ public class ProductService : IProductService .GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 50, cancellationToken).Result.Items.ToList(); var messagesForOpenAI = new List(); + messagesForOpenAI.Add(new OpenAiMessage() + { + Role = OpenAiRole.System.ToString().ToLower(), + Content = promptForGpt + }); foreach (var item in previousMessages ) { messagesForOpenAI.Add( @@ -128,18 +125,6 @@ public class ProductService : IProductService Text = messageBuffer.Text, }, cancellationToken); } - if (dataTypeHolder=="[Products]" && productBuffer.Name!=null) - { - _wishlistsService.AddProductToPersonalWishlistAsync(wishlistId, new ProductCreateDto() - { - Url = "", - Name = productBuffer.Name, - Rating = 0, - Description = "", - ImagesUrls = new []{"", ""}, - WasOpened = false - }, cancellationToken); - } dataTypeHolder = string.Empty; dataTypeHolder += data; } @@ -169,7 +154,6 @@ public class ProductService : IProductService break; case SearchEventType.Suggestion: - suggestionBuffer.Text += data; if (data.Contains(";")) { yield return new ServerSentEvent @@ -178,11 +162,12 @@ public class ProductService : IProductService Data = suggestionBuffer.Text }; suggestionBuffer.Text = string.Empty; + break; } + suggestionBuffer.Text += data; break; case SearchEventType.Product: - productBuffer.Name += data; if (data.Contains(";")) { yield return new ServerSentEvent @@ -191,7 +176,20 @@ public class ProductService : IProductService Data = productBuffer.Name }; productBuffer.Name = string.Empty; + + await _wishlistsService.AddProductToPersonalWishlistAsync(wishlistId, new ProductCreateDto() + { + Url = "", + Name = productBuffer.Name, + Rating = 0, + Description = "", + ImagesUrls = new []{"", ""}, + WasOpened = false + }, cancellationToken); + + break; } + productBuffer.Name += data; break; } } diff --git a/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs index 686d057..bb3119b 100644 --- a/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs +++ b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs @@ -25,8 +25,10 @@ public class ProductsTests : TestsBase // Act var response = await _httpClient.PostAsJsonAsync($"http://127.0.0.1:5183/api/ProductsSearch/search/{wishlistId}", message); - + var responseContent = await response.Content.ReadAsStringAsync(); + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(responseContent); } } \ No newline at end of file diff --git a/ShoppingAssistantApi.UnitTests/ProductTests.cs b/ShoppingAssistantApi.UnitTests/ProductTests.cs index 6bf5b67..f480031 100644 --- a/ShoppingAssistantApi.UnitTests/ProductTests.cs +++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs @@ -1,18 +1,14 @@ -using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; -using MongoDB.Bson; -using Moq; -using Newtonsoft.Json.Linq; +using Moq; using ShoppingAssistantApi.Application.IRepositories; using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.Models.CreateDtos; using ShoppingAssistantApi.Application.Models.Dtos; using ShoppingAssistantApi.Application.Models.OpenAi; -using ShoppingAssistantApi.Application.Models.ProductSearch; using ShoppingAssistantApi.Application.Paging; using ShoppingAssistantApi.Domain.Entities; using ShoppingAssistantApi.Domain.Enums; using ShoppingAssistantApi.Infrastructure.Services; +using System.Linq.Expressions; namespace ShoppingAssistantApi.Tests.Tests; @@ -24,21 +20,21 @@ public class ProductTests private Mock _wishListServiceMock; - private IMessagesRepository _messagesRepository; + private Mock _messagesRepositoryMock; - public ProductTests(IMessagesRepository messagesRepository) + public ProductTests() { - _messagesRepository = messagesRepository; + _messagesRepositoryMock = new Mock(); _openAiServiceMock = new Mock(); _wishListServiceMock = new Mock(); - _productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object, _messagesRepository); + _productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object, _messagesRepositoryMock.Object); } [Fact] - public async Task SearchProductAsync_WhenWishlistExists_ReturnsExpectedEvents() + public async Task SearchProductAsync_WhenWishlistsWithoutMessages_ReturnsExpectedEvents() { // Arrange - string wishlistId = "existingWishlistId"; // Simulating an existing wishlist ID + string wishlistId = "existingWishlistId"; var message = new MessageCreateDto { Text = "Your message text here" @@ -48,67 +44,44 @@ public class ProductTests // Define your expected SSE data for the test var expectedSseData = new List { - "[", - "Message", - "]", - " What", - " u", - " want", - " ?", - "[", - "Options", - "]", - " USB-C", - " ;", - " Keyboard", - " ultra", - " ;", - "?\n", - "[", - "Options", - "]", - " USB", - "-C", - " ;", - "[", - "Products", - "]", - " GTX", - " 3090", - " ;", - " GTX", - " 3070TI", - " ;", - " GTX", - " 4070TI", - " ;", - " ?" + "[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", " USB-C", " ;", " Keyboard", " ultra", + " ;", "[", "Options", "]", " USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX", + " 3070TI", " ;", " GTX", " 4070TI", " ;", " ?" }; + + var expectedMessages = new List { "What are you looking for?" }; // Mock the GetChatCompletionStream method to provide the expected SSE data _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) .Returns(expectedSseData.ToAsyncEnumerable()); - - _wishListServiceMock.Setup(w => w.GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, 1, cancellationToken)) - .ReturnsAsync(new PagedList(new List - { - new MessageDto - { - Text = "Some existing message", - Id = "", - CreatedById = "", - Role = "" - } - }, 1, 1, 1)); + + _messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny>>(), It.IsAny())) + .ReturnsAsync(0); + + _wishListServiceMock.Setup(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken)) + .Verifiable(); + // Act var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken); // Convert the result stream to a list of ServerSentEvent var actualSseEvents = await resultStream.ToListAsync(); + var receivedMessages = actualSseEvents + .Where(e => e.Event == SearchEventType.Message) + .Select(e => e.Data) + .ToList(); + + var receivedSuggestions = actualSseEvents + .Where(e => e.Event == SearchEventType.Suggestion) + .Select(e => e.Data) + .ToList(); + // Assert // Check if the actual SSE events match the expected SSE events Assert.NotNull(actualSseEvents); + Assert.Equal(expectedMessages, receivedMessages); + _wishListServiceMock.Verify(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken), Times.Once); } @@ -120,58 +93,165 @@ public class ProductTests var message = new MessageCreateDto { Text = "Your message text" }; var cancellationToken = new CancellationToken(); - var productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object, _messagesRepository); - + var productService = _productService; + var expectedSseData = new List { - "[", - "Message", - "]", - " What", - " u", - " want", - " ?", - "[", - "Options", - "]", - " USB-C", - " ;", - " Keyboard", - " ultra", - " ;", - "?\n", - "[", - "Options", - "]", - " USB", - "-C", - " ;", - "[", - "Products", - "]", - " GTX", - " 3090", - " ;", - " GTX", - " 3070TI", - " ;", - " GTX", - " 4070TI", - " ;", - " ?" + "[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", "USB-C", " ;", "Keyboard", " ultra", + " ;", "[", "Options", "]", "USB", "-C", " ;" }; + + var expectedMessages = new List { " What", " u", " want", " ?" }; + var expectedSuggestions = new List { "USB-C", "Keyboard ultra", "USB-C" }; // Mock the GetChatCompletionStream method to provide the expected SSE data _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) .Returns(expectedSseData.ToAsyncEnumerable()); - - // Act - var resultStream = productService.SearchProductAsync(wishlistId, message, cancellationToken); + _messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny>>(), It.IsAny())) + .ReturnsAsync(3); + + _wishListServiceMock.Setup(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken)) + .Verifiable(); + + _wishListServiceMock + .Setup(w => w.GetMessagesPageFromPersonalWishlistAsync( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList(new List + { + new MessageDto + { + Text = "Message 1", + Id = "1", + CreatedById = "User2", + Role = "User" + }, + new MessageDto + { + Text = "Message 2", + Id = "2", + CreatedById = "User2", + Role = "User" + }, + new MessageDto + { + Text = "Message 3", + Id = "3", + CreatedById = "User2", + Role = "User" + }, + }, 1, 3, 3)); + + // Act + var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken); + + // Convert the result stream to a list of ServerSentEvent var actualSseEvents = await resultStream.ToListAsync(); + + var receivedMessages = actualSseEvents + .Where(e => e.Event == SearchEventType.Message) + .Select(e => e.Data) + .ToList(); + + var receivedSuggestions = actualSseEvents + .Where(e => e.Event == SearchEventType.Suggestion) + .Select(e => e.Data) + .ToList(); // Assert Assert.NotNull(actualSseEvents); - Assert.Equal(3, actualSseEvents.Count); + Assert.Equal(expectedMessages, receivedMessages); + Assert.Equal(expectedSuggestions, receivedSuggestions); + _wishListServiceMock.Verify(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken), Times.Once); + } + + + [Fact] + public async void SearchProductAsync_WithExistingMessageInWishlistAndAddProduct_ReturnsExpectedEvents() + { + // Arrange + var wishlistId = "your_wishlist_id"; + var message = new MessageCreateDto { Text = "Your message text" }; + var cancellationToken = new CancellationToken(); + + var productService = _productService; + + var expectedSseData = new List + { + "[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", "USB-C", " ;", "Keyboard", " ultra", + " ;", "[", "Options", "]", "USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX", + " 3070TI", " ;", " GTX", " 4070TI", " ;", " ?" + }; + + var expectedMessages = new List { " What", " u", " want", " ?" }; + var expectedSuggestions = new List { "USB-C", "Keyboard ultra", "USB-C" }; + + // Mock the GetChatCompletionStream method to provide the expected SSE data + _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny(), cancellationToken)) + .Returns(expectedSseData.ToAsyncEnumerable()); + + _messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny>>(), It.IsAny())) + .ReturnsAsync(3); + + _wishListServiceMock + .Setup(w => w.AddProductToPersonalWishlistAsync( + It.IsAny(), It.IsAny(), It.IsAny())) + .Verifiable(); + + _wishListServiceMock.Setup(w => w.AddProductToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken)) + .Verifiable(); + + _wishListServiceMock + .Setup(w => w.GetMessagesPageFromPersonalWishlistAsync( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList(new List + { + new MessageDto + { + Text = "Message 1", + Id = "1", + CreatedById = "User2", + Role = "User" + }, + new MessageDto + { + Text = "Message 2", + Id = "2", + CreatedById = "User2", + Role = "User" + }, + new MessageDto + { + Text = "Message 3", + Id = "3", + CreatedById = "User2", + Role = "User" + }, + }, 1, 3, 3)); + + // Act + var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken); + + // Convert the result stream to a list of ServerSentEvent + var actualSseEvents = await resultStream.ToListAsync(); + + var receivedMessages = actualSseEvents + .Where(e => e.Event == SearchEventType.Message) + .Select(e => e.Data) + .ToList(); + + var receivedSuggestions = actualSseEvents + .Where(e => e.Event == SearchEventType.Suggestion) + .Select(e => e.Data) + .ToList(); + + // Assert + Assert.NotNull(actualSseEvents); + Assert.Equal(expectedMessages, receivedMessages); + Assert.Equal(expectedSuggestions, receivedSuggestions); + _wishListServiceMock.Verify(w => w.AddProductToPersonalWishlistAsync( + It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _wishListServiceMock.Verify(w => w.AddMessageToPersonalWishlistAsync( + wishlistId, It.IsAny(), cancellationToken), Times.Once); } } \ No newline at end of file