Merge pull request 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
ShoppingAssistantApi.Api
ShoppingAssistantApi.Application
ShoppingAssistantApi.Domain/Enums
ShoppingAssistantApi.Infrastructure/Services
ShoppingAssistantApi.Persistance/Repositories
ShoppingAssistantApi.UnitTests

View File

@ -8,7 +8,6 @@ using ShoppingAssistantApi.Domain.Enums;
namespace ShoppingAssistantApi.Api.Controllers;
[Authorize]
public class ProductsSearchController : BaseController
{
private readonly IProductService _productService;
@ -21,16 +20,10 @@ public class ProductsSearchController : BaseController
_wishlistsService = wishlistsService;
}
[Authorize]
[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("Cache-Control", "no-cache");
Response.Headers.Add("Connection", "keep-alive");
@ -43,8 +36,8 @@ public class ProductsSearchController : BaseController
var serverSentEvent = $"event: {sse.Event}\ndata: {chunk}\n\n";
await Response.WriteAsync(serverSentEvent);
await Response.Body.FlushAsync();
await Response.WriteAsync(serverSentEvent, cancellationToken: cancellationToken);
await Response.Body.FlushAsync(cancellationToken);
}
}

View File

@ -2,6 +2,7 @@
using ShoppingAssistantApi.Application.Models.Dtos;
using ShoppingAssistantApi.Application.Models.Operations;
using HotChocolate.Authorization;
using ShoppingAssistantApi.Application.IServices;
namespace ShoppingAssistantApi.Api.Mutations;
@ -27,4 +28,12 @@ public class UsersMutation
public Task<UserDto> RemoveFromRoleAsync(string roleName, string userId, CancellationToken cancellationToken,
[Service] IUserManager userManager)
=> 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 MongoDB.Bson;
using ShoppingAssistantApi.Domain.Entities;
namespace ShoppingAssistantApi.Application.IRepositories;
@ -6,4 +7,6 @@ namespace ShoppingAssistantApi.Application.IRepositories;
public interface IMessagesRepository : IBaseRepository<Message>
{
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 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 string RequestConvert(this OpenAiRole role)
public static string ToRequestString(this OpenAiRole role)
{
switch (role)
return role switch
{
case OpenAiRole.System:
return "system";
case OpenAiRole.Assistant:
return "assistant";
case OpenAiRole.User:
return "user";
default:
return "";
}
OpenAiRole.System => "system",
OpenAiRole.Assistant => "assistant",
OpenAiRole.User => "user",
_ => "",
};
}
}

View File

@ -1,8 +1,7 @@
using System.IO;
using System.Net.Http.Headers;
using System.Text;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using ShoppingAssistantApi.Application.IServices;
using ShoppingAssistantApi.Application.Models.OpenAi;
@ -23,9 +22,14 @@ public class OpenAiService : IOpenAiService
private readonly HttpClient _httpClient;
public OpenAiService(IHttpClientFactory httpClientFactory)
private readonly ILogger<OpenAiService> _logger;
public OpenAiService(
IHttpClientFactory httpClientFactory,
ILogger<OpenAiService> logger)
{
_httpClient = httpClientFactory.CreateClient("OpenAiHttpClient");
_logger = logger;
}
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)
{
_logger.LogInformation($"Sending completion stream request to OpenAI.");
chat.Stream = true;
var jsonBody = JsonConvert.SerializeObject(chat, _jsonSettings);
var body = new StringContent(jsonBody, Encoding.UTF8, "application/json");
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 request = new HttpRequestMessage(HttpMethod.Post, "")
{
var jsonChunk = await reader.ReadLineAsync();
if (jsonChunk.StartsWith("data: "))
Content = body
};
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)
{
var line = await streamReader.ReadLineAsync(cancellationToken);
allData += line + "\n\n";
if (string.IsNullOrEmpty(line)) continue;
var json = line?.Substring(6, line.Length - 6);
if (json == "[DONE]")
{
jsonChunk = jsonChunk.Substring("data: ".Length);
if (jsonChunk == "[DONE]") break;
var data = JsonConvert.DeserializeObject<OpenAiResponse>(jsonChunk);
if (data.Choices[0].Delta.Content == "" || data.Choices[0].Delta.Content == null) continue;
yield return data.Choices[0].Delta.Content;
yield return json;
yield break;
}
var data = JsonConvert.DeserializeObject<OpenAiResponse>(json);
if (data.Choices[0].Delta.Content == "" || data.Choices[0].Delta.Content == null) continue;
yield return data.Choices[0].Delta.Content;
}
}
}

View File

@ -1,4 +1,4 @@
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using ShoppingAssistantApi.Application.IRepositories;
using ShoppingAssistantApi.Application.IServices;
@ -6,7 +6,6 @@ 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;
@ -15,126 +14,128 @@ namespace ShoppingAssistantApi.Infrastructure.Services;
public class ProductService : IProductService
{
private readonly IWishlistsService _wishlistsService;
private readonly IOpenAiService _openAiService;
private readonly IMessagesRepository _messagesRepository;
private bool mqchecker = false;
private SearchEventType currentDataType = SearchEventType.Wishlist;
public ProductService(IOpenAiService openAiService, IWishlistsService wishlistsService, IMessagesRepository messagesRepository)
private readonly ILogger<ProductService> _logger;
public ProductService(
IOpenAiService openAiService,
IWishlistsService wishlistsService,
IMessagesRepository messagesRepository,
ILogger<ProductService> logger)
{
_openAiService = openAiService;
_wishlistsService = wishlistsService;
_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." +
"\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[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 wishlistObjectId = ObjectId.Parse(wishlistId);
var messages = await _messagesRepository.GetWishlistMessagesAsync(wishlistObjectId, cancellationToken);
var chatRequest = new ChatCompletionRequest
{
Messages = new List<OpenAiMessage>
{
new OpenAiMessage
{
Role = OpenAiRoleExtensions.RequestConvert(OpenAiRole.System),
Content = promptForGpt
new() {
Role = OpenAiRole.System.ToRequestString(),
Content = systemPrompt
}
}
};
var messagesForOpenAI = new List<OpenAiMessage>();
foreach (var item in previousMessages.Items)
for (int i = 0; i < messages.Count; i++)
{
if (item.Role == "Application")
var message = messages[i];
if (i == 0)
{
messagesForOpenAI
.Add(new OpenAiMessage()
{
Role = OpenAiRole.Assistant.RequestConvert(),
Content = item.Text
});
}
else
{
messagesForOpenAI
.Add(new OpenAiMessage()
{
Role = item.Role.ToLower(),
Content = item.Text
});
message.Text = "[Question] " + message.Text + "\n [Suggestions] Bicycle, Laptop";
}
chatRequest.Messages
.Add(new OpenAiMessage()
{
Role = message.Role == "Application" ? "assistant" : "user",
Content = message.Text
});
}
messagesForOpenAI.Add(new OpenAiMessage()
chatRequest.Messages.Add(new ()
{
Role = OpenAiRoleExtensions.RequestConvert(OpenAiRole.User),
Content = message.Text
Role = OpenAiRole.User.ToRequestString(),
Content = newMessage.Text
});
chatRequest.Messages.AddRange(messagesForOpenAI);
// 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 messageBuffer = new MessagePart();
var productBuffer = new ProductName();
var dataTypeHolder = string.Empty;
var counter = 0;
await foreach (var data in _openAiService.GetChatCompletionStream(chatRequest, cancellationToken))
{
counter++;
if (mqchecker && currentDataType == SearchEventType.Message && messageBuffer != null)
if (data == "[DONE]")
{
if (data == "[")
if (!string.IsNullOrEmpty(messageBuffer.Text))
{
_wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageDto()
_ = await _wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, new MessageDto()
{
Text = messageBuffer.Text,
Role = MessageRoles.Application.ToString(),
}, cancellationToken);
mqchecker = false;
}
}
if (data.Contains("["))
{
dataTypeHolder = string.Empty;
dataTypeHolder += data;
}
else if (data.Contains("]"))
yield break;
}
else if (data.Contains('['))
{
dataTypeHolder = data;
}
else if (data.Contains(']'))
{
dataTypeHolder += data;
currentDataType = DetermineDataType(dataTypeHolder);
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);
else if (dataTypeHolder=="[" && !data.Contains("["))
messageBuffer.Text = string.Empty;
}
dataTypeHolder += data;
currentDataType = DetermineDataType(dataTypeHolder);
dataTypeHolder = string.Empty;
}
else if (dataTypeHolder.Contains('['))
{
dataTypeHolder += data;
}
else
{
switch (currentDataType)
@ -147,47 +148,44 @@ public class ProductService : IProductService
};
currentDataType = SearchEventType.Message;
messageBuffer.Text += data;
break;
case SearchEventType.Suggestion:
if (data.Contains(";"))
if (data.Contains(';'))
{
yield return new ServerSentEvent
{
Event = SearchEventType.Suggestion,
Data = suggestionBuffer.Text
Data = suggestionBuffer.Text.Trim()
};
suggestionBuffer.Text = string.Empty;
break;
}
suggestionBuffer.Text += data;
break;
case SearchEventType.Product:
if (data.Contains(";"))
if (data.Contains(';'))
{
yield return new ServerSentEvent
{
Event = SearchEventType.Product,
Data = productBuffer.Name
Data = productBuffer.Name.Trim()
};
productBuffer.Name = string.Empty;
break;
}
productBuffer.Name += data;
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)
@ -196,7 +194,7 @@ public class ProductService : IProductService
{
return SearchEventType.Message;
}
else if (dataTypeHolder.StartsWith("[Options]"))
else if (dataTypeHolder.StartsWith("[Suggestions]"))
{
return SearchEventType.Suggestion;
}
@ -213,5 +211,4 @@ public class ProductService : IProductService
return SearchEventType.Wishlist;
}
}
}

View File

@ -59,4 +59,26 @@ public class UsersService : IUsersService
entity.LastModifiedDateUtc = DateTime.UtcNow;
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
{
Role = OpenAiRole.System.RequestConvert(),
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"
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. 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
{
Role = OpenAiRole.User.RequestConvert(),
Role = OpenAiRole.User.ToRequestString(),
Content = firstUserMessage.Text
}
}

View File

@ -1,4 +1,5 @@
using System.Linq.Expressions;
using MongoDB.Bson;
using MongoDB.Driver;
using ShoppingAssistantApi.Application.IRepositories;
using ShoppingAssistantApi.Domain.Entities;
@ -18,4 +19,11 @@ public class MessagesRepository : BaseRepository<Message>, IMessagesRepository
.Limit(pageSize)
.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 Microsoft.Extensions.Logging;
using Moq;
using Moq.Protected;
using ShoppingAssistantApi.Application.IServices;
@ -31,7 +32,7 @@ public class OpenAiServiceTests
return client;
});
_openAiService = new OpenAiService(_mockHttpClientFactory.Object);
_openAiService = new OpenAiService(_mockHttpClientFactory.Object, new Mock<ILogger<OpenAiService>>().Object);
}
[Fact]
@ -78,7 +79,7 @@ public class OpenAiServiceTests
{
new OpenAiMessage
{
Role = OpenAiRole.User.RequestConvert(),
Role = OpenAiRole.User.ToRequestString(),
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.IServices;
using ShoppingAssistantApi.Application.Models.CreateDtos;
@ -27,14 +29,14 @@ public class ProductTests
_messagesRepositoryMock = new Mock<IMessagesRepository>();
_openAiServiceMock = new Mock<IOpenAiService>();
_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]
public async Task SearchProductAsync_WhenWishlistsWithoutMessages_ReturnsExpectedEvents()
{
// Arrange
string wishlistId = "existingWishlistId";
string wishlistId = "657657677c13ae4bc95e2f41";
var message = new MessageCreateDto
{
Text = "Your message text here"
@ -44,47 +46,28 @@ public class ProductTests
// Define your expected SSE data for the test
var expectedSseData = new List<string>
{
"[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", " USB-C", " ;", " Keyboard", " ultra",
" ;", "[", "Options", "]", " USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX",
"[", "Message", "]", " What", " u", " want", " ?", "[", "Suggestions", "]", " USB-C", " ;", " Keyboard", " ultra",
" ;", "[", "Suggestions", "]", " USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX",
" 3070TI", " ;", " GTX", " 4070TI", " ;", " ?", "[", "Message", "]", " What", " u", " want", " ?"
};
var expectedMessages = new List<string> { " What", " u", " want", " ?", " What", " u", " want", " ?" };
var expectedSuggestion = new List<string> { " USB-C", " Keyboard ultra", " USB-C" };
var expectedSuggestion = new List<string> { "USB-C", "Keyboard ultra", "USB-C" };
// Mock the GetChatCompletionStream method to provide the expected SSE data
_openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny<ChatCompletionRequest>(), cancellationToken))
.Returns(expectedSseData.ToAsyncEnumerable());
_messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny<Expression<Func<Message, bool>>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(1);
_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>
_messagesRepositoryMock.Setup(m => m.GetWishlistMessagesAsync(It.IsAny<ObjectId>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Message>
{
new MessageDto
new()
{
Text = "What are you looking for?",
Id = "3",
CreatedById = "User2",
Role = "User"
},
},
1,
1,
1
));
});
// Act
var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken);
@ -113,7 +96,7 @@ public class ProductTests
public async void SearchProductAsync_WithExistingMessageInWishlist_ReturnsExpectedEvents()
{
// Arrange
var wishlistId = "your_wishlist_id";
var wishlistId = "657657677c13ae4bc95e2f41";
var message = new MessageCreateDto { Text = "Your message text" };
var cancellationToken = new CancellationToken();
@ -121,8 +104,8 @@ public class ProductTests
var expectedSseData = new List<string>
{
"[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", "USB-C", " ;", "Keyboard", " ultra",
" ;", "[", "Options", "]", "USB", "-C", " ;"
"[", "Message", "]", " What", " u", " want", " ?", "[", "Suggestions", "]", "USB-C", " ;", "Keyboard", " ultra",
" ;", "[", "Suggestions", "]", "USB", "-C", " ;"
};
var expectedMessages = new List<string> { " What", " u", " want", " ?" };
@ -131,40 +114,25 @@ public class ProductTests
// Mock the GetChatCompletionStream method to provide the expected SSE data
_openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny<ChatCompletionRequest>(), cancellationToken))
.Returns(expectedSseData.ToAsyncEnumerable());
_messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny<Expression<Func<Message, bool>>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(3);
_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
_messagesRepositoryMock.Setup(m => m.GetWishlistMessagesAsync(It.IsAny<ObjectId>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Message>
{
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));
new() {
Text = "Message 1",
Role = "User"
},
new Message
{
Text = "Message 2",
Role = "User"
},
new Message
{
Text = "Message 3",
Role = "User"
},
});
// Act
var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken);
@ -186,7 +154,6 @@ public class ProductTests
Assert.NotNull(actualSseEvents);
Assert.Equal(expectedMessages, receivedMessages);
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()
{
// Arrange
var wishlistId = "your_wishlist_id";
var wishlistId = "657657677c13ae4bc95e2f41";
var message = new MessageCreateDto { Text = "Your message text" };
var cancellationToken = new CancellationToken();
@ -202,8 +169,8 @@ public class ProductTests
var expectedSseData = new List<string>
{
"[", "Message", "]", " What", " u", " want", " ?", "[", "Options", "]", "USB-C", " ;", "Keyboard", " ultra",
" ;", "[", "Options", "]", "USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX",
"[", "Message", "]", " What", " u", " want", " ?", "[", "Suggestions", "]", "USB-C", " ;", "Keyboard", " ultra",
" ;", "[", "Suggestions", "]", "USB", "-C", " ;", "[", "Products", "]", " GTX", " 3090", " ;", " GTX",
" 3070TI", " ;", " GTX", " 4070TI", " ;", " ?"
};
@ -214,36 +181,25 @@ public class ProductTests
_openAiServiceMock.Setup(x => x.GetChatCompletionStream(It.IsAny<ChatCompletionRequest>(), cancellationToken))
.Returns(expectedSseData.ToAsyncEnumerable());
_messagesRepositoryMock.Setup(m => m.GetCountAsync(It.IsAny<Expression<Func<Message, bool>>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(3);
_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
_messagesRepositoryMock.Setup(m => m.GetWishlistMessagesAsync(It.IsAny<ObjectId>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Message>
{
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));
new()
{
Text = "Message 1",
Role = "User"
},
new()
{
Text = "Message 2",
Role = "User"
},
new()
{
Text = "Message 3",
Role = "User"
},
});
// Act
var resultStream = _productService.SearchProductAsync(wishlistId, message, cancellationToken);
@ -265,7 +221,5 @@ public class ProductTests
Assert.NotNull(actualSseEvents);
Assert.Equal(expectedMessages, receivedMessages);
Assert.Equal(expectedSuggestions, receivedSuggestions);
_wishListServiceMock.Verify(w => w.AddMessageToPersonalWishlistAsync(
wishlistId, It.IsAny<MessageDto>(), cancellationToken), Times.Once);
}
}