Merge pull request #27 from Shchoholiev/release/v1.0.0

Release/v1.0.0
This commit is contained in:
Serhii Shchoholiev 2023-12-19 18:08:27 -08:00 committed by GitHub
commit 9a79645730
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 231 additions and 228 deletions

View File

@ -8,7 +8,6 @@ using ShoppingAssistantApi.Domain.Enums;
namespace ShoppingAssistantApi.Api.Controllers; namespace ShoppingAssistantApi.Api.Controllers;
[Authorize]
public class ProductsSearchController : BaseController public class ProductsSearchController : BaseController
{ {
private readonly IProductService _productService; private readonly IProductService _productService;
@ -21,16 +20,10 @@ public class ProductsSearchController : BaseController
_wishlistsService = wishlistsService; _wishlistsService = wishlistsService;
} }
[Authorize]
[HttpPost("search/{wishlistId}")] [HttpPost("search/{wishlistId}")]
public async Task StreamDataToClient(string wishlistId, [FromBody] MessageCreateDto message, CancellationToken cancellationToken) public async Task StreamDataToClient(string wishlistId, [FromBody] MessageCreateDto message, CancellationToken cancellationToken)
{ {
var dto = new MessageDto()
{
Text = message.Text,
Role = MessageRoles.User.ToString(),
};
await _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, dto, cancellationToken);
Response.Headers.Add("Content-Type", "text/event-stream"); Response.Headers.Add("Content-Type", "text/event-stream");
Response.Headers.Add("Cache-Control", "no-cache"); Response.Headers.Add("Cache-Control", "no-cache");
Response.Headers.Add("Connection", "keep-alive"); Response.Headers.Add("Connection", "keep-alive");
@ -43,8 +36,8 @@ public class ProductsSearchController : BaseController
var serverSentEvent = $"event: {sse.Event}\ndata: {chunk}\n\n"; var serverSentEvent = $"event: {sse.Event}\ndata: {chunk}\n\n";
await Response.WriteAsync(serverSentEvent); await Response.WriteAsync(serverSentEvent, cancellationToken: cancellationToken);
await Response.Body.FlushAsync(); await Response.Body.FlushAsync(cancellationToken);
} }
} }

View File

@ -2,6 +2,7 @@
using ShoppingAssistantApi.Application.Models.Dtos; using ShoppingAssistantApi.Application.Models.Dtos;
using ShoppingAssistantApi.Application.Models.Operations; using ShoppingAssistantApi.Application.Models.Operations;
using HotChocolate.Authorization; using HotChocolate.Authorization;
using ShoppingAssistantApi.Application.IServices;
namespace ShoppingAssistantApi.Api.Mutations; namespace ShoppingAssistantApi.Api.Mutations;
@ -27,4 +28,12 @@ public class UsersMutation
public Task<UserDto> RemoveFromRoleAsync(string roleName, string userId, CancellationToken cancellationToken, public Task<UserDto> RemoveFromRoleAsync(string roleName, string userId, CancellationToken cancellationToken,
[Service] IUserManager userManager) [Service] IUserManager userManager)
=> userManager.RemoveFromRoleAsync(roleName, userId, cancellationToken); => userManager.RemoveFromRoleAsync(roleName, userId, cancellationToken);
[Authorize]
public async Task<bool> DeletePersonalUserAsync(string guestId, CancellationToken cancellationToken,
[Service] IUsersService usersService)
{
await usersService.DeletePersonalUserAsync(guestId, cancellationToken);
return true;
}
} }

View File

@ -1,4 +1,5 @@
using System.Linq.Expressions; using System.Linq.Expressions;
using MongoDB.Bson;
using ShoppingAssistantApi.Domain.Entities; using ShoppingAssistantApi.Domain.Entities;
namespace ShoppingAssistantApi.Application.IRepositories; namespace ShoppingAssistantApi.Application.IRepositories;
@ -6,4 +7,6 @@ namespace ShoppingAssistantApi.Application.IRepositories;
public interface IMessagesRepository : IBaseRepository<Message> public interface IMessagesRepository : IBaseRepository<Message>
{ {
Task<List<Message>> GetPageStartingFromEndAsync(int pageNumber, int pageSize, Expression<Func<Message, bool>> predicate, CancellationToken cancellationToken); Task<List<Message>> GetPageStartingFromEndAsync(int pageNumber, int pageSize, Expression<Func<Message, bool>> predicate, CancellationToken cancellationToken);
Task<List<Message>> GetWishlistMessagesAsync(ObjectId wishlistId, CancellationToken cancellationToken);
} }

View File

@ -12,4 +12,6 @@ public interface IUsersService
Task<UserDto> GetUserAsync(string id, CancellationToken cancellationToken); Task<UserDto> GetUserAsync(string id, CancellationToken cancellationToken);
Task UpdateUserAsync(UserDto dto, CancellationToken cancellationToken); Task UpdateUserAsync(UserDto dto, CancellationToken cancellationToken);
Task DeletePersonalUserAsync(string guestId, CancellationToken cancellationToken);
} }

View File

@ -9,18 +9,14 @@ public enum OpenAiRole
public static class OpenAiRoleExtensions public static class OpenAiRoleExtensions
{ {
public static string RequestConvert(this OpenAiRole role) public static string ToRequestString(this OpenAiRole role)
{ {
switch (role) return role switch
{ {
case OpenAiRole.System: OpenAiRole.System => "system",
return "system"; OpenAiRole.Assistant => "assistant",
case OpenAiRole.Assistant: OpenAiRole.User => "user",
return "assistant"; _ => "",
case OpenAiRole.User: };
return "user";
default:
return "";
}
} }
} }

View File

@ -1,8 +1,7 @@
using System.IO;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.IServices;
using ShoppingAssistantApi.Application.Models.OpenAi; using ShoppingAssistantApi.Application.Models.OpenAi;
@ -23,9 +22,14 @@ public class OpenAiService : IOpenAiService
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
public OpenAiService(IHttpClientFactory httpClientFactory) private readonly ILogger<OpenAiService> _logger;
public OpenAiService(
IHttpClientFactory httpClientFactory,
ILogger<OpenAiService> logger)
{ {
_httpClient = httpClientFactory.CreateClient("OpenAiHttpClient"); _httpClient = httpClientFactory.CreateClient("OpenAiHttpClient");
_logger = logger;
} }
public async Task<OpenAiMessage> GetChatCompletion(ChatCompletionRequest chat, CancellationToken cancellationToken) public async Task<OpenAiMessage> GetChatCompletion(ChatCompletionRequest chat, CancellationToken cancellationToken)
@ -45,27 +49,41 @@ public class OpenAiService : IOpenAiService
public async IAsyncEnumerable<string> GetChatCompletionStream(ChatCompletionRequest chat, CancellationToken cancellationToken) public async IAsyncEnumerable<string> GetChatCompletionStream(ChatCompletionRequest chat, CancellationToken cancellationToken)
{ {
_logger.LogInformation($"Sending completion stream request to OpenAI.");
chat.Stream = true; chat.Stream = true;
var jsonBody = JsonConvert.SerializeObject(chat, _jsonSettings); var jsonBody = JsonConvert.SerializeObject(chat, _jsonSettings);
var body = new StringContent(jsonBody, Encoding.UTF8, "application/json"); var body = new StringContent(jsonBody, Encoding.UTF8, "application/json");
var request = new HttpRequestMessage(HttpMethod.Post, "")
using var httpResponse = await _httpClient.PostAsync("", body, cancellationToken);
using var responseStream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken);
using var reader = new StreamReader(responseStream, Encoding.UTF8);
while (!cancellationToken.IsCancellationRequested)
{ {
var jsonChunk = await reader.ReadLineAsync(); Content = body
if (jsonChunk.StartsWith("data: ")) };
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
using var httpResponse = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
var allData = string.Empty;
using var streamReader = new StreamReader(await httpResponse.Content.ReadAsStreamAsync(cancellationToken));
while (!streamReader.EndOfStream)
{ {
jsonChunk = jsonChunk.Substring("data: ".Length); var line = await streamReader.ReadLineAsync(cancellationToken);
if (jsonChunk == "[DONE]") break; allData += line + "\n\n";
var data = JsonConvert.DeserializeObject<OpenAiResponse>(jsonChunk); if (string.IsNullOrEmpty(line)) continue;
var json = line?.Substring(6, line.Length - 6);
if (json == "[DONE]")
{
yield return json;
yield break;
}
var data = JsonConvert.DeserializeObject<OpenAiResponse>(json);
if (data.Choices[0].Delta.Content == "" || data.Choices[0].Delta.Content == null) continue; if (data.Choices[0].Delta.Content == "" || data.Choices[0].Delta.Content == null) continue;
yield return data.Choices[0].Delta.Content; yield return data.Choices[0].Delta.Content;
} }
} }
} }
}

View File

@ -1,4 +1,4 @@
using System.Diagnostics; using Microsoft.Extensions.Logging;
using MongoDB.Bson; using MongoDB.Bson;
using ShoppingAssistantApi.Application.IRepositories; using ShoppingAssistantApi.Application.IRepositories;
using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.IServices;
@ -6,7 +6,6 @@ using ShoppingAssistantApi.Application.Models.CreateDtos;
using ShoppingAssistantApi.Application.Models.Dtos; using ShoppingAssistantApi.Application.Models.Dtos;
using ShoppingAssistantApi.Application.Models.OpenAi; using ShoppingAssistantApi.Application.Models.OpenAi;
using ShoppingAssistantApi.Application.Models.ProductSearch; using ShoppingAssistantApi.Application.Models.ProductSearch;
using ShoppingAssistantApi.Domain.Entities;
using ShoppingAssistantApi.Domain.Enums; using ShoppingAssistantApi.Domain.Enums;
using ServerSentEvent = ShoppingAssistantApi.Application.Models.ProductSearch.ServerSentEvent; using ServerSentEvent = ShoppingAssistantApi.Application.Models.ProductSearch.ServerSentEvent;
@ -20,121 +19,123 @@ public class ProductService : IProductService
private readonly IMessagesRepository _messagesRepository; private readonly IMessagesRepository _messagesRepository;
private bool mqchecker = false; private readonly ILogger<ProductService> _logger;
private SearchEventType currentDataType = SearchEventType.Wishlist; public ProductService(
IOpenAiService openAiService,
public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService, IMessagesRepository messagesRepository) IWishlistsService wishlistsService,
IMessagesRepository messagesRepository,
ILogger<ProductService> logger)
{ {
_openAiService = openAiService; _openAiService = openAiService;
_wishlistsService = wishlistsService; _wishlistsService = wishlistsService;
_messagesRepository = messagesRepository; _messagesRepository = messagesRepository;
_logger = logger;
} }
public async IAsyncEnumerable<ServerSentEvent> SearchProductAsync(string wishlistId, MessageCreateDto message, CancellationToken cancellationToken) public async IAsyncEnumerable<ServerSentEvent> SearchProductAsync(string wishlistId, MessageCreateDto newMessage, CancellationToken cancellationToken)
{ {
string promptForGpt = var systemPrompt =
"You are a Shopping Assistant that helps people find product recommendations. Ask user additional questions if more context needed." + "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:" + "\nYou must return data with one of the prefixes:" +
"\n[Question] - return question" + "\n[Question] - return question. Must be followed by suggestions how to answer the question" +
"\n[Suggestions] - return semicolon separated suggestion how to answer to a question" + "\n[Suggestions] - return semicolon separated suggestion how to answer to a question" +
"\n[Message] - return text" + "\n[Message] - return text" +
"\n[Products] - return semicolon separated product names"; "\n[Products] - return semicolon separated product names";
var countOfMessage = await _messagesRepository var wishlistObjectId = ObjectId.Parse(wishlistId);
.GetCountAsync(message=>message.WishlistId == ObjectId.Parse((wishlistId)), cancellationToken); var messages = await _messagesRepository.GetWishlistMessagesAsync(wishlistObjectId, cancellationToken);
var previousMessages = await _wishlistsService
.GetMessagesPageFromPersonalWishlistAsync(wishlistId, 1, countOfMessage, cancellationToken);
var chatRequest = new ChatCompletionRequest var chatRequest = new ChatCompletionRequest
{ {
Messages = new List<OpenAiMessage> Messages = new List<OpenAiMessage>
{ {
new OpenAiMessage new() {
{ Role = OpenAiRole.System.ToRequestString(),
Role = OpenAiRoleExtensions.RequestConvert(OpenAiRole.System), Content = systemPrompt
Content = promptForGpt
} }
} }
}; };
for (int i = 0; i < messages.Count; i++)
var messagesForOpenAI = new List<OpenAiMessage>();
foreach (var item in previousMessages.Items)
{ {
if (item.Role == "Application") var message = messages[i];
if (i == 0)
{ {
messagesForOpenAI message.Text = "[Question] " + message.Text + "\n [Suggestions] Bicycle, Laptop";
}
chatRequest.Messages
.Add(new OpenAiMessage() .Add(new OpenAiMessage()
{ {
Role = OpenAiRole.Assistant.RequestConvert(), Role = message.Role == "Application" ? "assistant" : "user",
Content = item.Text
});
}
else
{
messagesForOpenAI
.Add(new OpenAiMessage()
{
Role = item.Role.ToLower(),
Content = item.Text
});
}
}
messagesForOpenAI.Add(new OpenAiMessage()
{
Role = OpenAiRoleExtensions.RequestConvert(OpenAiRole.User),
Content = message.Text Content = message.Text
}); });
}
chatRequest.Messages.AddRange(messagesForOpenAI); chatRequest.Messages.Add(new ()
{
Role = OpenAiRole.User.ToRequestString(),
Content = newMessage.Text
});
// Don't wait for the task to finish because we dont need the result of this task
var dto = new MessageDto()
{
Text = newMessage.Text,
Role = MessageRoles.User.ToString(),
};
var saveNewMessageTask = _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, dto, cancellationToken);
var currentDataType = SearchEventType.Wishlist;
var suggestionBuffer = new Suggestion(); var suggestionBuffer = new Suggestion();
var messageBuffer = new MessagePart(); var messageBuffer = new MessagePart();
var productBuffer = new ProductName(); var productBuffer = new ProductName();
var dataTypeHolder = string.Empty; var dataTypeHolder = string.Empty;
var counter = 0;
await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken)) await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken))
{ {
counter++; if (data == "[DONE]")
if (mqchecker && currentDataType == SearchEventType.Message && messageBuffer != null)
{ {
if (data == "[") if (!string.IsNullOrEmpty(messageBuffer.Text))
{ {
_wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageDto() _ = await _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageDto()
{ {
Text = messageBuffer.Text, Text = messageBuffer.Text,
Role = MessageRoles.Application.ToString(), Role = MessageRoles.Application.ToString(),
}, cancellationToken); }, cancellationToken);
mqchecker = false;
}
} }
if (data.Contains("[")) yield break;
{
dataTypeHolder = string.Empty;
dataTypeHolder += data;
} }
else if (data.Contains('['))
else if (data.Contains("]")) {
dataTypeHolder = data;
}
else if (data.Contains(']'))
{ {
dataTypeHolder += data;
currentDataType = DetermineDataType(dataTypeHolder);
if (currentDataType == SearchEventType.Message) if (currentDataType == SearchEventType.Message)
{ {
mqchecker = true; _ = await saveNewMessageTask;
} // Don't wait for the task to finish because we dont need the result of this task
_ = _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageDto()
{
Text = messageBuffer.Text,
Role = MessageRoles.Application.ToString(),
}, cancellationToken);
messageBuffer.Text = string.Empty;
} }
else if (dataTypeHolder=="[" && !data.Contains("[")) dataTypeHolder += data;
currentDataType = DetermineDataType(dataTypeHolder);
dataTypeHolder = string.Empty;
}
else if (dataTypeHolder.Contains('['))
{ {
dataTypeHolder += data; dataTypeHolder += data;
} }
else else
{ {
switch (currentDataType) switch (currentDataType)
@ -147,47 +148,44 @@ public class ProductService : IProductService
}; };
currentDataType = SearchEventType.Message; currentDataType = SearchEventType.Message;
messageBuffer.Text += data; messageBuffer.Text += data;
break; break;
case SearchEventType.Suggestion: case SearchEventType.Suggestion:
if (data.Contains(";")) if (data.Contains(';'))
{ {
yield return new ServerSentEvent yield return new ServerSentEvent
{ {
Event = SearchEventType.Suggestion, Event = SearchEventType.Suggestion,
Data = suggestionBuffer.Text Data = suggestionBuffer.Text.Trim()
}; };
suggestionBuffer.Text = string.Empty; suggestionBuffer.Text = string.Empty;
break; break;
} }
suggestionBuffer.Text += data; suggestionBuffer.Text += data;
break; break;
case SearchEventType.Product: case SearchEventType.Product:
if (data.Contains(";")) if (data.Contains(';'))
{ {
yield return new ServerSentEvent yield return new ServerSentEvent
{ {
Event = SearchEventType.Product, Event = SearchEventType.Product,
Data = productBuffer.Name Data = productBuffer.Name.Trim()
}; };
productBuffer.Name = string.Empty; productBuffer.Name = string.Empty;
break; break;
} }
productBuffer.Name += data; productBuffer.Name += data;
break; break;
} }
} }
} }
if (currentDataType == SearchEventType.Message)
{
_wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageDto()
{
Text = messageBuffer.Text,
Role = MessageRoles.Application.ToString(),
}, cancellationToken);
mqchecker = false;
}
} }
private SearchEventType DetermineDataType(string dataTypeHolder) private SearchEventType DetermineDataType(string dataTypeHolder)
@ -196,7 +194,7 @@ public class ProductService : IProductService
{ {
return SearchEventType.Message; return SearchEventType.Message;
} }
else if (dataTypeHolder.StartsWith("[Options]")) else if (dataTypeHolder.StartsWith("[Suggestions]"))
{ {
return SearchEventType.Suggestion; return SearchEventType.Suggestion;
} }
@ -213,5 +211,4 @@ public class ProductService : IProductService
return SearchEventType.Wishlist; return SearchEventType.Wishlist;
} }
} }
} }

View File

@ -59,4 +59,26 @@ public class UsersService : IUsersService
entity.LastModifiedDateUtc = DateTime.UtcNow; entity.LastModifiedDateUtc = DateTime.UtcNow;
await _repository.UpdateUserAsync(entity, cancellationToken); await _repository.UpdateUserAsync(entity, cancellationToken);
} }
public async Task DeletePersonalUserAsync(string guestId, CancellationToken cancellationToken)
{
if (!Guid.TryParse(guestId, out var guid))
{
throw new InvalidDataException("Provided id is invalid.");
}
var entity = await _repository.GetUserAsync(u => u.GuestId == guid, cancellationToken);
if (entity.Id != GlobalUser.Id)
{
throw new UnAuthorizedException<User>();
}
if (entity == null)
{
throw new EntityNotFoundException<User>();
}
await _repository.DeleteAsync(entity, cancellationToken);
}
} }

View File

@ -82,12 +82,12 @@ public class WishlistsService : IWishlistsService
{ {
new OpenAiMessage new OpenAiMessage
{ {
Role = OpenAiRole.System.RequestConvert(), Role = OpenAiRole.System.ToRequestString(),
Content = "You will be provided with a general information about some product and your task is to generate general (not specific to any company or brand) chat name where recommendations on which specific product to buy will be given. Only name he product without adverbs and adjectives\nExamples:\n - Prompt: Hub For Macbook. Answer: Macbook Hub\n - Prompt: What is the best power bank for MacBook with capacity 20000 mAh and power near 20V? Answer: Macbook Powerbank" Content = "You will be provided with a general information about some product and your task is to generate general (not specific to any company or brand) chat name where recommendations on which specific product to buy will be given. Only name he product without adverbs and adjectives. Limit the name length to 5 words\nExamples:\n - Prompt: Hub For Macbook. Answer: Macbook Hub\n - Prompt: What is the best power bank for MacBook with capacity 20000 mAh and power near 20V? Answer: Macbook Powerbank\nIf the information tells nothing about some product answer with short generic name"
}, },
new OpenAiMessage new OpenAiMessage
{ {
Role = OpenAiRole.User.RequestConvert(), Role = OpenAiRole.User.ToRequestString(),
Content = firstUserMessage.Text Content = firstUserMessage.Text
} }
} }

View File

@ -1,4 +1,5 @@
using System.Linq.Expressions; using System.Linq.Expressions;
using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using ShoppingAssistantApi.Application.IRepositories; using ShoppingAssistantApi.Application.IRepositories;
using ShoppingAssistantApi.Domain.Entities; using ShoppingAssistantApi.Domain.Entities;
@ -18,4 +19,11 @@ public class MessagesRepository : BaseRepository<Message>, IMessagesRepository
.Limit(pageSize) .Limit(pageSize)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
} }
public Task<List<Message>> GetWishlistMessagesAsync(ObjectId wishlistId, CancellationToken cancellationToken)
{
return _collection
.Find(x => !x.IsDeleted && x.WishlistId == wishlistId)
.ToListAsync(cancellationToken);
}
} }

View File

@ -1,4 +1,5 @@
using System.Net; using System.Net;
using Microsoft.Extensions.Logging;
using Moq; using Moq;
using Moq.Protected; using Moq.Protected;
using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.IServices;
@ -31,7 +32,7 @@ public class OpenAiServiceTests
return client; return client;
}); });
_openAiService = new OpenAiService(_mockHttpClientFactory.Object); _openAiService = new OpenAiService(_mockHttpClientFactory.Object, new Mock<ILogger<OpenAiService>>().Object);
} }
[Fact] [Fact]
@ -78,7 +79,7 @@ public class OpenAiServiceTests
{ {
new OpenAiMessage new OpenAiMessage
{ {
Role = OpenAiRole.User.RequestConvert(), Role = OpenAiRole.User.ToRequestString(),
Content = "Return Hello World!" Content = "Return Hello World!"
} }
} }

View File

@ -1,4 +1,6 @@
using Moq; using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using Moq;
using ShoppingAssistantApi.Application.IRepositories; using ShoppingAssistantApi.Application.IRepositories;
using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.IServices;
using ShoppingAssistantApi.Application.Models.CreateDtos; using ShoppingAssistantApi.Application.Models.CreateDtos;
@ -27,14 +29,14 @@ public class ProductTests
_messagesRepositoryMock = new Mock<IMessagesRepository>(); _messagesRepositoryMock = new Mock<IMessagesRepository>();
_openAiServiceMock = new Mock<IOpenAiService>(); _openAiServiceMock = new Mock<IOpenAiService>();
_wishListServiceMock = new Mock<IWishlistsService>(); _wishListServiceMock = new Mock<IWishlistsService>();
_productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object, _messagesRepositoryMock.Object); _productService = new ProductService(_openAiServiceMock.Object, _wishListServiceMock.Object, _messagesRepositoryMock.Object, new Mock<ILogger<ProductService>>().Object);
} }
[Fact] [Fact]
public async Task SearchProductAsync_WhenWishlistsWithoutMessages_ReturnsExpectedEvents() public async Task SearchProductAsync_WhenWishlistsWithoutMessages_ReturnsExpectedEvents()
{ {
// Arrange // Arrange
string wishlistId = "existingWishlistId"; string wishlistId = "657657677c13ae4bc95e2f41";
var message = new MessageCreateDto var message = new MessageCreateDto
{ {
Text = "Your message text here" Text = "Your message text here"
@ -44,8 +46,8 @@ public class ProductTests
// Define your expected SSE data for the test // Define your expected SSE data for the test
var expectedSseData = new List<string> var expectedSseData = new List<string>
{ {
"[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", " USB-C", " ;", " Keyboard", " ultra", "[", "Message", "]", " What", " u", " want", " ?", "[", "Suggestions", "]", " USB-C", " ;", " Keyboard", " ultra",
" ;", "[", "Options", "]", " USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX", " ;", "[", "Suggestions", "]", " USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX",
" 3070TI", " ;", " GTX", " 4070TI", " ;", " ?", "[", "Message", "]", " What", " u", " want", " ?" " 3070TI", " ;", " GTX", " 4070TI", " ;", " ?", "[", "Message", "]", " What", " u", " want", " ?"
}; };
@ -56,34 +58,15 @@ public class ProductTests
_openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny<ChatCompletionRequest>(), cancellationToken)) _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny<ChatCompletionRequest>(), cancellationToken))
.Returns(expectedSseData.ToAsyncEnumerable()); .Returns(expectedSseData.ToAsyncEnumerable());
_messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny<Expression<Func<Message, bool>>>(), It.IsAny<CancellationToken>())) _messagesRepositoryMock.Setup(m => m.GetWishlistMessagesAsync(It.IsAny<ObjectId>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(1); .ReturnsAsync(new List<Message>
_wishListServiceMock.Setup(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny<MessageDto>(), cancellationToken))
.Verifiable();
_wishListServiceMock
.Setup(m => m.GetMessagesPageFromPersonalWishlistAsync(
It.IsAny<string>(),
It.IsAny<int>(),
It.IsAny<int>(),
It.IsAny<CancellationToken>())
)
.ReturnsAsync(new PagedList<MessageDto>(
new List<MessageDto>
{ {
new MessageDto new()
{ {
Text = "What are you looking for?", Text = "What are you looking for?",
Id = "3",
CreatedById = "User2",
Role = "User" Role = "User"
}, },
}, });
1,
1,
1
));
// Act // Act
var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken); var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken);
@ -113,7 +96,7 @@ public class ProductTests
public async void SearchProductAsync_WithExistingMessageInWishlist_ReturnsExpectedEvents() public async void SearchProductAsync_WithExistingMessageInWishlist_ReturnsExpectedEvents()
{ {
// Arrange // Arrange
var wishlistId = "your_wishlist_id"; var wishlistId = "657657677c13ae4bc95e2f41";
var message = new MessageCreateDto { Text = "Your message text" }; var message = new MessageCreateDto { Text = "Your message text" };
var cancellationToken = new CancellationToken(); var cancellationToken = new CancellationToken();
@ -121,8 +104,8 @@ public class ProductTests
var expectedSseData = new List<string> var expectedSseData = new List<string>
{ {
"[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", "USB-C", " ;", "Keyboard", " ultra", "[", "Message", "]", " What", " u", " want", " ?", "[", "Suggestions", "]", "USB-C", " ;", "Keyboard", " ultra",
" ;", "[", "Options", "]", "USB", "-C", " ;" " ;", "[", "Suggestions", "]", "USB", "-C", " ;"
}; };
var expectedMessages = new List<string> { " What", " u", " want", " ?" }; var expectedMessages = new List<string> { " What", " u", " want", " ?" };
@ -132,39 +115,24 @@ public class ProductTests
_openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny<ChatCompletionRequest>(), cancellationToken)) _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny<ChatCompletionRequest>(), cancellationToken))
.Returns(expectedSseData.ToAsyncEnumerable()); .Returns(expectedSseData.ToAsyncEnumerable());
_messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny<Expression<Func<Message, bool>>>(), It.IsAny<CancellationToken>())) _messagesRepositoryMock.Setup(m => m.GetWishlistMessagesAsync(It.IsAny<ObjectId>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(3); .ReturnsAsync(new List<Message>
_wishListServiceMock.Setup(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny<MessageDto>(), cancellationToken))
.Verifiable();
_wishListServiceMock
.Setup(w => w.GetMessagesPageFromPersonalWishlistAsync(
It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PagedList<MessageDto>(new List<MessageDto>
{
new MessageDto
{ {
new() {
Text = "Message 1", Text = "Message 1",
Id = "1",
CreatedById = "User2",
Role = "User" Role = "User"
}, },
new MessageDto new Message
{ {
Text = "Message 2", Text = "Message 2",
Id = "2",
CreatedById = "User2",
Role = "User" Role = "User"
}, },
new MessageDto new Message
{ {
Text = "Message 3", Text = "Message 3",
Id = "3",
CreatedById = "User2",
Role = "User" Role = "User"
}, },
}, 1, 3, 3)); });
// Act // Act
var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken); var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken);
@ -186,7 +154,6 @@ public class ProductTests
Assert.NotNull(actualSseEvents); Assert.NotNull(actualSseEvents);
Assert.Equal(expectedMessages, receivedMessages); Assert.Equal(expectedMessages, receivedMessages);
Assert.Equal(expectedSuggestions, receivedSuggestions); Assert.Equal(expectedSuggestions, receivedSuggestions);
_wishListServiceMock.Verify(w => w.AddMessageToPersonalWishlistAsync(wishlistId, It.IsAny<MessageDto>(), cancellationToken), Times.Once);
} }
@ -194,7 +161,7 @@ public class ProductTests
public async void SearchProductAsync_WithExistingMessageInWishlistAndAddProduct_ReturnsExpectedEvents() public async void SearchProductAsync_WithExistingMessageInWishlistAndAddProduct_ReturnsExpectedEvents()
{ {
// Arrange // Arrange
var wishlistId = "your_wishlist_id"; var wishlistId = "657657677c13ae4bc95e2f41";
var message = new MessageCreateDto { Text = "Your message text" }; var message = new MessageCreateDto { Text = "Your message text" };
var cancellationToken = new CancellationToken(); var cancellationToken = new CancellationToken();
@ -202,8 +169,8 @@ public class ProductTests
var expectedSseData = new List<string> var expectedSseData = new List<string>
{ {
"[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", "USB-C", " ;", "Keyboard", " ultra", "[", "Message", "]", " What", " u", " want", " ?", "[", "Suggestions", "]", "USB-C", " ;", "Keyboard", " ultra",
" ;", "[", "Options", "]", "USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX", " ;", "[", "Suggestions", "]", "USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX",
" 3070TI", " ;", " GTX", " 4070TI", " ;", " ?" " 3070TI", " ;", " GTX", " 4070TI", " ;", " ?"
}; };
@ -214,36 +181,25 @@ public class ProductTests
_openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny<ChatCompletionRequest>(), cancellationToken)) _openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny<ChatCompletionRequest>(), cancellationToken))
.Returns(expectedSseData.ToAsyncEnumerable()); .Returns(expectedSseData.ToAsyncEnumerable());
_messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny<Expression<Func<Message, bool>>>(), It.IsAny<CancellationToken>())) _messagesRepositoryMock.Setup(m => m.GetWishlistMessagesAsync(It.IsAny<ObjectId>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(3); .ReturnsAsync(new List<Message>
_wishListServiceMock
.Setup(w => w.GetMessagesPageFromPersonalWishlistAsync(
It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PagedList<MessageDto>(new List<MessageDto>
{ {
new MessageDto new()
{ {
Text = "Message 1", Text = "Message 1",
Id = "1",
CreatedById = "User2",
Role = "User" Role = "User"
}, },
new MessageDto new()
{ {
Text = "Message 2", Text = "Message 2",
Id = "2",
CreatedById = "User2",
Role = "User" Role = "User"
}, },
new MessageDto new()
{ {
Text = "Message 3", Text = "Message 3",
Id = "3",
CreatedById = "User2",
Role = "User" Role = "User"
}, },
}, 1, 3, 3)); });
// Act // Act
var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken); var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken);
@ -265,7 +221,5 @@ public class ProductTests
Assert.NotNull(actualSseEvents); Assert.NotNull(actualSseEvents);
Assert.Equal(expectedMessages, receivedMessages); Assert.Equal(expectedMessages, receivedMessages);
Assert.Equal(expectedSuggestions, receivedSuggestions); Assert.Equal(expectedSuggestions, receivedSuggestions);
_wishListServiceMock.Verify(w => w.AddMessageToPersonalWishlistAsync(
wishlistId, It.IsAny<MessageDto>(), cancellationToken), Times.Once);
} }
} }