diff --git a/ShoppingAssistantApi.Api/Controllers/BaseController.cs b/ShoppingAssistantApi.Api/Controllers/BaseController.cs
new file mode 100644
index 0000000..87efa2c
--- /dev/null
+++ b/ShoppingAssistantApi.Api/Controllers/BaseController.cs
@@ -0,0 +1,10 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace ShoppingAssistantApi.Api.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class BaseController : ControllerBase
+{
+
+}
\ No newline at end of file
diff --git a/ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs b/ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs
new file mode 100644
index 0000000..7dd9949
--- /dev/null
+++ b/ShoppingAssistantApi.Api/Controllers/ProductsSearchController.cs
@@ -0,0 +1,39 @@
+using HotChocolate.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json;
+using ShoppingAssistantApi.Application.IServices;
+using ShoppingAssistantApi.Application.Models.CreateDtos;
+
+namespace ShoppingAssistantApi.Api.Controllers;
+
+[Authorize]
+public class ProductsSearchController : BaseController
+{
+ private readonly IProductService _productService;
+
+ public ProductsSearchController(IProductService productService)
+ {
+ _productService = productService;
+ }
+
+ [HttpPost("search/{wishlistId}")]
+ public async Task StreamDataToClient(string wishlistId, [FromBody]MessageCreateDto message, CancellationToken cancellationToken)
+ {
+ Response.Headers.Add("Content-Type", "text/event-stream");
+ Response.Headers.Add("Cache-Control", "no-cache");
+ Response.Headers.Add("Connection", "keep-alive");
+
+ var result = _productService.SearchProductAsync(wishlistId, message, cancellationToken);
+
+ await foreach (var sse in result)
+ {
+ var chunk = JsonConvert.SerializeObject(sse.Data);
+
+ var serverSentEvent = $"event: {sse.Event}\ndata: {chunk}\n\n";
+
+ await Response.WriteAsync(serverSentEvent);
+ await Response.Body.FlushAsync();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/ShoppingAssistantApi.Api/ShoppingAssistantApi.Api.csproj b/ShoppingAssistantApi.Api/ShoppingAssistantApi.Api.csproj
index 50dd57a..761a282 100644
--- a/ShoppingAssistantApi.Api/ShoppingAssistantApi.Api.csproj
+++ b/ShoppingAssistantApi.Api/ShoppingAssistantApi.Api.csproj
@@ -21,8 +21,5 @@
-
-
-
diff --git a/ShoppingAssistantApi.Application/IServices/IProductService.cs b/ShoppingAssistantApi.Application/IServices/IProductService.cs
new file mode 100644
index 0000000..3cf6d42
--- /dev/null
+++ b/ShoppingAssistantApi.Application/IServices/IProductService.cs
@@ -0,0 +1,12 @@
+using ShoppingAssistantApi.Application.Models.CreateDtos;
+using ShoppingAssistantApi.Application.Models.Dtos;
+using ShoppingAssistantApi.Application.Models.ProductSearch;
+using ShoppingAssistantApi.Domain.Entities;
+
+namespace ShoppingAssistantApi.Application.IServices;
+
+public interface IProductService
+{
+ IAsyncEnumerable SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken);
+
+}
\ No newline at end of file
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.Application/Models/ProductSearch/MessagePart.cs b/ShoppingAssistantApi.Application/Models/ProductSearch/MessagePart.cs
new file mode 100644
index 0000000..feacc20
--- /dev/null
+++ b/ShoppingAssistantApi.Application/Models/ProductSearch/MessagePart.cs
@@ -0,0 +1,6 @@
+namespace ShoppingAssistantApi.Application.Models.ProductSearch;
+
+public class MessagePart
+{
+ public string Text { get; set; }
+}
diff --git a/ShoppingAssistantApi.Application/Models/ProductSearch/ProductName.cs b/ShoppingAssistantApi.Application/Models/ProductSearch/ProductName.cs
new file mode 100644
index 0000000..559ab3d
--- /dev/null
+++ b/ShoppingAssistantApi.Application/Models/ProductSearch/ProductName.cs
@@ -0,0 +1,6 @@
+namespace ShoppingAssistantApi.Application.Models.ProductSearch;
+
+public class ProductName
+{
+ public string Name { get; set; }
+}
\ No newline at end of file
diff --git a/ShoppingAssistantApi.Application/Models/ProductSearch/Question.cs b/ShoppingAssistantApi.Application/Models/ProductSearch/Question.cs
new file mode 100644
index 0000000..e1f0c46
--- /dev/null
+++ b/ShoppingAssistantApi.Application/Models/ProductSearch/Question.cs
@@ -0,0 +1,6 @@
+namespace ShoppingAssistantApi.Application.Models.ProductSearch;
+
+public class Question
+{
+ public string QuestionText { get; set; }
+}
\ No newline at end of file
diff --git a/ShoppingAssistantApi.Application/Models/ProductSearch/ServerSentEvent.cs b/ShoppingAssistantApi.Application/Models/ProductSearch/ServerSentEvent.cs
new file mode 100644
index 0000000..e7be1cb
--- /dev/null
+++ b/ShoppingAssistantApi.Application/Models/ProductSearch/ServerSentEvent.cs
@@ -0,0 +1,10 @@
+using ShoppingAssistantApi.Domain.Enums;
+
+namespace ShoppingAssistantApi.Application.Models.ProductSearch;
+
+public class ServerSentEvent
+{
+ public SearchEventType Event { get; set; }
+
+ public string Data { get; set; }
+}
diff --git a/ShoppingAssistantApi.Application/Models/ProductSearch/Suggestion.cs b/ShoppingAssistantApi.Application/Models/ProductSearch/Suggestion.cs
new file mode 100644
index 0000000..ccefea2
--- /dev/null
+++ b/ShoppingAssistantApi.Application/Models/ProductSearch/Suggestion.cs
@@ -0,0 +1,6 @@
+namespace ShoppingAssistantApi.Application.Models.ProductSearch;
+
+public class Suggestion
+{
+ public string Text { get; set; }
+}
diff --git a/ShoppingAssistantApi.Application/ShoppingAssistantApi.Application.csproj b/ShoppingAssistantApi.Application/ShoppingAssistantApi.Application.csproj
index 46083d4..e7b7e9f 100644
--- a/ShoppingAssistantApi.Application/ShoppingAssistantApi.Application.csproj
+++ b/ShoppingAssistantApi.Application/ShoppingAssistantApi.Application.csproj
@@ -16,4 +16,5 @@
+
diff --git a/ShoppingAssistantApi.Domain/Entities/Wishlist.cs b/ShoppingAssistantApi.Domain/Entities/Wishlist.cs
index 4dee7fc..9c9bfc6 100644
--- a/ShoppingAssistantApi.Domain/Entities/Wishlist.cs
+++ b/ShoppingAssistantApi.Domain/Entities/Wishlist.cs
@@ -7,6 +7,4 @@ public class Wishlist : EntityBase
public string Name { get; set; }
public string Type { get; set; }
-
- public ICollection? Messages { get; set; }
}
diff --git a/ShoppingAssistantApi.Domain/Enums/SearchEventType.cs b/ShoppingAssistantApi.Domain/Enums/SearchEventType.cs
new file mode 100644
index 0000000..3388692
--- /dev/null
+++ b/ShoppingAssistantApi.Domain/Enums/SearchEventType.cs
@@ -0,0 +1,24 @@
+namespace ShoppingAssistantApi.Domain.Enums;
+
+public enum SearchEventType
+{
+ Wishlist = 0,
+ Message = 1,
+ Suggestion = 2,
+ Product = 3
+}
+
+public static class SearchEventTypeExtensions
+{
+ public static string ToSseEventString(this SearchEventType eventType)
+ {
+ return eventType switch
+ {
+ SearchEventType.Wishlist => "wishlist",
+ SearchEventType.Message => "message",
+ SearchEventType.Suggestion => "suggestion",
+ SearchEventType.Product => "product",
+ _ => throw new ArgumentOutOfRangeException(nameof(eventType), eventType, null),
+ };
+ }
+}
diff --git a/ShoppingAssistantApi.Infrastructure/InfrastructureExtentions/ServicesExtention.cs b/ShoppingAssistantApi.Infrastructure/InfrastructureExtentions/ServicesExtention.cs
index 32aa136..e3df82c 100644
--- a/ShoppingAssistantApi.Infrastructure/InfrastructureExtentions/ServicesExtention.cs
+++ b/ShoppingAssistantApi.Infrastructure/InfrastructureExtentions/ServicesExtention.cs
@@ -18,6 +18,7 @@ public static class ServicesExtention
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
return services;
}
diff --git a/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs
new file mode 100644
index 0000000..8c4d567
--- /dev/null
+++ b/ShoppingAssistantApi.Infrastructure/Services/ProductService.cs
@@ -0,0 +1,191 @@
+using System.Diagnostics;
+using MongoDB.Bson;
+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.Domain.Entities;
+using ShoppingAssistantApi.Domain.Enums;
+using ServerSentEvent = ShoppingAssistantApi.Application.Models.ProductSearch.ServerSentEvent;
+
+namespace ShoppingAssistantApi.Infrastructure.Services;
+
+public class ProductService : IProductService
+{
+ private readonly IWishlistsService _wishlistsService;
+
+ private readonly IOpenAiService _openAiService;
+
+ private readonly IMessagesRepository _messagesRepository;
+
+ public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService, IMessagesRepository messagesRepository)
+ {
+ _openAiService = openAiService;
+ _wishlistsService = wishlistsService;
+ _messagesRepository = messagesRepository;
+ }
+
+ 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 countOfMessage = await _messagesRepository
+ .GetCountAsync(message=>message.WishlistId==ObjectId.Parse((wishlistId)), cancellationToken);
+
+ var previousMessages = await _wishlistsService
+ .GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, countOfMessage, cancellationToken);
+
+ var chatRequest = new ChatCompletionRequest
+ {
+ Messages = new List
+ {
+ new OpenAiMessage
+ {
+ Role = OpenAiRoleExtensions.RequestConvert(OpenAiRole.System),
+ Content = promptForGpt
+ }
+ }
+ };
+
+
+ var messagesForOpenAI = new List();
+
+ foreach (var item in previousMessages.Items)
+ {
+ messagesForOpenAI
+ .Add(new OpenAiMessage()
+ {
+ Role = item.Role.ToLower(),
+ Content = item.Text
+ });
+ }
+
+ messagesForOpenAI.Add(new OpenAiMessage()
+ {
+ Role = OpenAiRoleExtensions.RequestConvert(OpenAiRole.User),
+ Content = message.Text
+ });
+
+ chatRequest.Messages.AddRange(messagesForOpenAI);
+
+ var suggestionBuffer = new Suggestion();
+ var messageBuffer = new MessagePart();
+ var productBuffer = new ProductName();
+ var currentDataType = SearchEventType.Wishlist;
+ var dataTypeHolder = string.Empty;
+
+ await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken))
+ {
+ if (data.Contains("["))
+ {
+ if (dataTypeHolder=="[Message]" && messageBuffer.Text!=null)
+ {
+ _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageCreateDto()
+ {
+ Text = messageBuffer.Text,
+ }, cancellationToken);
+ }
+ dataTypeHolder = string.Empty;
+ dataTypeHolder += data;
+ }
+
+ else if (data.Contains("]"))
+ {
+ dataTypeHolder += data;
+ currentDataType = DetermineDataType(dataTypeHolder);
+ }
+
+ else if (dataTypeHolder=="[" && !data.Contains("["))
+ {
+ dataTypeHolder += data;
+ }
+
+ else
+ {
+ switch (currentDataType)
+ {
+ case SearchEventType.Message:
+ yield return new ServerSentEvent
+ {
+ Event = SearchEventType.Message,
+ Data = data
+ };
+ messageBuffer.Text += data;
+ break;
+
+ case SearchEventType.Suggestion:
+ if (data.Contains(";"))
+ {
+ yield return new ServerSentEvent
+ {
+ Event = SearchEventType.Suggestion,
+ Data = suggestionBuffer.Text
+ };
+ suggestionBuffer.Text = string.Empty;
+ break;
+ }
+ suggestionBuffer.Text += data;
+ break;
+
+ case SearchEventType.Product:
+ if (data.Contains(";"))
+ {
+ yield return new ServerSentEvent
+ {
+ Event = SearchEventType.Product,
+ Data = productBuffer.Name
+ };
+ productBuffer.Name = string.Empty;
+
+ //a complete description of the entity when the Amazon API is connected
+ await _wishlistsService.AddProductToPersonalWishlistAsync(wishlistId, new ProductCreateDto()
+ {
+ Url = "",
+ Name = productBuffer.Name,
+ Rating = 0,
+ Description = "",
+ ImagesUrls = new []{"", ""},
+ WasOpened = false
+ }, cancellationToken);
+ break;
+ }
+ productBuffer.Name += data;
+ break;
+ }
+ }
+ }
+ }
+
+ private SearchEventType DetermineDataType(string dataTypeHolder)
+ {
+ if (dataTypeHolder.StartsWith("[Question]"))
+ {
+ return SearchEventType.Message;
+ }
+ else if (dataTypeHolder.StartsWith("[Options]"))
+ {
+ return SearchEventType.Suggestion;
+ }
+ else if (dataTypeHolder.StartsWith("[Message]"))
+ {
+ return SearchEventType.Message;
+ }
+ else if (dataTypeHolder.StartsWith("[Products]"))
+ {
+ return SearchEventType.Product;
+ }
+ else
+ {
+ return SearchEventType.Wishlist;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs b/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs
index f627b46..98425a6 100644
--- a/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs
+++ b/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs
@@ -190,25 +190,6 @@ public class DbInitialaizer
Name = "Gaming PC",
Type = WishlistTypes.Product.ToString(),
CreatedById = user1.Id,
- Messages = new Message[]
- {
- new Message
- {
- Text = "Prompt",
- Role = MessageRoles.User.ToString(),
- WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab65cd"),
- CreatedById = user1.Id,
- CreatedDateUtc = DateTime.UtcNow
- },
- new Message
- {
- Text = "Answer",
- Role = MessageRoles.Application.ToString(),
- WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab65cd"),
- CreatedById = user1.Id,
- CreatedDateUtc = DateTime.UtcNow
- },
- }
},
new Wishlist
{
@@ -216,17 +197,6 @@ public class DbInitialaizer
Name = "Generic Wishlist Name",
Type = WishlistTypes.Product.ToString(),
CreatedById = user2.Id,
- Messages = new Message[]
- {
- new Message
- {
- Text = "Prompt",
- Role = MessageRoles.User.ToString(),
- WishlistId = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"),
- CreatedById = user1.Id,
- CreatedDateUtc = DateTime.UtcNow
- }
- }
}
};
diff --git a/ShoppingAssistantApi.Tests/ShoppingAssistantApi.Tests.csproj b/ShoppingAssistantApi.Tests/ShoppingAssistantApi.Tests.csproj
index c36f3b4..85fc768 100644
--- a/ShoppingAssistantApi.Tests/ShoppingAssistantApi.Tests.csproj
+++ b/ShoppingAssistantApi.Tests/ShoppingAssistantApi.Tests.csproj
@@ -13,6 +13,7 @@
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs b/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs
index 811cce6..02929da 100644
--- a/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs
+++ b/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs
@@ -107,6 +107,8 @@ public class DbInitializer
var wishlistId1 = ObjectId.Parse("ab79cde6f69abcd3efab65cd");
var wishlistId2 = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab");
+ var wishlistId3 = ObjectId.Parse("ab7c8c2d9edf39abcd1ef9ab");
+ var wishlistId4 = ObjectId.Parse("ab8c8c2d9edf39abcd1ef9ab");
var wishlists = new Wishlist[]
{
@@ -125,7 +127,23 @@ public class DbInitializer
Type = WishlistTypes.Product.ToString(),
CreatedById = user2.Id,
CreatedDateUtc = DateTime.UtcNow
- }
+ },
+ new Wishlist
+ {
+ Id = wishlistId3,
+ Name = "Test For Search",
+ Type = WishlistTypes.Product.ToString(),
+ CreatedById = user1.Id,
+ CreatedDateUtc = DateTime.UtcNow
+ },
+ new Wishlist
+ {
+ Id = wishlistId4,
+ Name = "Test For Answer",
+ Type = WishlistTypes.Product.ToString(),
+ CreatedById = user1.Id,
+ CreatedDateUtc = DateTime.UtcNow
+ },
};
await wishlistsCollection.InsertManyAsync(wishlists);
@@ -142,6 +160,8 @@ public class DbInitializer
var wishlistId1 = ObjectId.Parse("ab79cde6f69abcd3efab65cd");
var wishlistId2 = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab");
+ var wishlistId3 = ObjectId.Parse("ab7c8c2d9edf39abcd1ef9ab");
+ var wishlistId4 = ObjectId.Parse("ab8c8c2d9edf39abcd1ef9ab");
var messages = new Message[]
{
@@ -197,7 +217,23 @@ public class DbInitializer
WishlistId = wishlistId2,
CreatedById = user2.Id,
CreatedDateUtc = DateTime.UtcNow
- }
+ },
+ new Message
+ {
+ Text = "What are you looking for?",
+ Role = "assistant",
+ WishlistId = wishlistId4,
+ CreatedById = user2.Id,
+ CreatedDateUtc = DateTime.UtcNow
+ },
+ new Message
+ {
+ Text = "What are you looking for?",
+ Role = "assistant",
+ WishlistId = wishlistId3,
+ CreatedById = user2.Id,
+ CreatedDateUtc = DateTime.UtcNow
+ },
};
await messagesCollection.InsertManyAsync(messages);
diff --git a/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs
new file mode 100644
index 0000000..f9756ec
--- /dev/null
+++ b/ShoppingAssistantApi.Tests/Tests/ProductsTests.cs
@@ -0,0 +1,70 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Text;
+using MongoDB.Bson;
+using Newtonsoft.Json;
+using ShoppingAssistantApi.Application.Models.CreateDtos;
+using ShoppingAssistantApi.Tests.TestExtentions;
+
+namespace ShoppingAssistantApi.Tests.Tests;
+
+public class ProductsTests : TestsBase
+{
+ public ProductsTests(TestingFactory factory)
+ : base(factory)
+ {
+ }
+
+ [Fact]
+ public async Task StreamDataToClient_ReturnsExpectedResponse()
+ {
+ await LoginAsync("wishlists@gmail.com", "Yuiop12345");
+ // Arrange
+ var wishlistId = "ab8c8c2d9edf39abcd1ef9ab";
+ var message = new MessageCreateDto { Text = "I want new powerful laptop" };
+
+ // 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);
+ }
+
+ [Fact]
+ public async Task StreamDataToClientFirstly_ReturnsExpectedResponse()
+ {
+ await LoginAsync("wishlists@gmail.com", "Yuiop12345");
+ // Arrange
+ var wishlistId = "ab7c8c2d9edf39abcd1ef9ab";
+ var message = new MessageCreateDto { Text = "I want new powerful laptop" };
+
+ // Act
+ var response = await _httpClient.PostAsJsonAsync($"http://127.0.0.1:5183/api/ProductsSearch/search/{wishlistId}", message);
+ var responseContent = await response.Content.ReadAsStringAsync();
+ var sseEvents = responseContent.Split("\n\n", StringSplitOptions.RemoveEmptyEntries);
+ bool foundMessageEvent = false;
+
+ // Assert
+ foreach (var sseEvent in sseEvents)
+ {
+ var sseParts = sseEvent.Split('\n');
+ if (sseParts.Length >= 2)
+ {
+ var eventName = sseParts[0];
+ var eventData = sseParts[1].Substring("data: ".Length);
+ if (eventName == "event: Message")
+ {
+ foundMessageEvent = true;
+ Assert.NotNull(eventData);
+ break;
+ }
+ }
+ }
+
+ Assert.True(foundMessageEvent, "Message event not found");
+ 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
new file mode 100644
index 0000000..9887d90
--- /dev/null
+++ b/ShoppingAssistantApi.UnitTests/ProductTests.cs
@@ -0,0 +1,281 @@
+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.Paging;
+using ShoppingAssistantApi.Domain.Entities;
+using ShoppingAssistantApi.Domain.Enums;
+using ShoppingAssistantApi.Infrastructure.Services;
+using System.Linq.Expressions;
+
+namespace ShoppingAssistantApi.Tests.Tests;
+
+public class ProductTests
+{
+ private Mock _openAiServiceMock;
+
+ private IProductService _productService;
+
+ private Mock _wishListServiceMock;
+
+ private Mock _messagesRepositoryMock;
+
+ public ProductTests()
+ {
+ _messagesRepositoryMock = new Mock();
+ _openAiServiceMock = new Mock();
+ _wishListServiceMock = new Mock();
+ _productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object, _messagesRepositoryMock.Object);
+ }
+
+ [Fact]
+ public async Task SearchProductAsync_WhenWishlistsWithoutMessages_ReturnsExpectedEvents()
+ {
+ // Arrange
+ string wishlistId = "existingWishlistId";
+ var message = new MessageCreateDto
+ {
+ Text = "Your message text here"
+ };
+ var cancellationToken = CancellationToken.None;
+
+ // Define your expected SSE data for the test
+ 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 expectedSuggestion = 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(1);
+
+ _wishListServiceMock.Setup(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny(), cancellationToken))
+ .Verifiable();
+
+ _wishListServiceMock
+ .Setup(m => m.GetMessagesPageFromPersonalWishlistAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny())
+ )
+ .ReturnsAsync(new PagedList(
+ new List
+ {
+ new MessageDto
+ {
+ Text = "What are you looking for?",
+ Id = "3",
+ CreatedById = "User2",
+ Role = "User"
+ },
+ },
+ 1,
+ 1,
+ 1
+ ));
+
+ // 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);
+ Assert.Equal(expectedSuggestion, receivedSuggestions);
+ }
+
+
+ [Fact]
+ public async void SearchProductAsync_WithExistingMessageInWishlist_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", " ;"
+ };
+
+ 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.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(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
diff --git a/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj b/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj
index 9274a65..05ef7fc 100644
--- a/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj
+++ b/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj
@@ -10,8 +10,10 @@
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -23,9 +25,9 @@
-
-
-
+
+
+