diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f97bf2bc..e2f55779 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -42,6 +42,12 @@ Once database is setup you can run :
> :warning: **You have to generate keystore.jks before!**, see [README](https://github.com/ditrit/leto-modelizer-api/blob/main/README.md#Generate-certificate-for-HTTPS).
+If you do not want to have a real AI, you can use a fake AI:
+```shell
+docker build -t leto-modelizer-ai-proxy -f ./src/test/resources/ai/Dockerfile ./src/test/resources/ai
+docker run -p 8585:8585 -v $(pwd)/src/test/resources/ai:/var/www/html --restart always leto-modelizer-ai-proxy
+```
+
## Run application tests
To run all the application tests (unit and integration), use this command:
diff --git a/README.md b/README.md
index 5b4d503c..e1d3f516 100644
--- a/README.md
+++ b/README.md
@@ -245,7 +245,7 @@ enabling secure and streamlined user authentication.
| CSRF_TOKEN_TIMEOUT | No, default: `3600` | A configuration parameter that specifies the duration (in seconds) for which a Cross-Site Request Forgery (CSRF) token remains valid. This setting is used to prevent CSRF attacks by ensuring that the token used in a client session expires after a certain period, requiring a new token for future requests. |
| USER_SESSION_TIMEOUT | No, default: `3600` | A configuration parameter that defines the time (in seconds) a user's session remains active without any activity. After this period, the user is automatically logged out to help protect against unauthorized access and to manage server resource utilization efficiently. |
| SUPER_ADMINISTRATOR_LOGIN | No | A configuration parameter that defines the username on Github of the SUPER_ADMINISTRATOR. It will create user if it doesn't exist and associate it to the `SUPER_ADMINISTRATOR` role. |
-| AI_HOST | No | A configuration parameter that defines the host of the ia server, example: http://localhost:8001/api/. If it's not set, users will not be approve to use ia in application. |
+| AI_HOST | No, default: `http://localhost:8585/` | A configuration parameter that defines the host of the ia server, example: http://localhost:8585/api/. If it's not set, users will not be approve to use ia in application. |
> Notes: `GITHUB_ENTERPRISE_*` variables are only required on self-hosted GitHub.
@@ -275,7 +275,7 @@ LETO_ADMIN_URL=http://localhost:9000/
LIBRARY_HOST_WHITELIST=https://github.com/ditrit/
CSRF_TOKEN_TIMEOUT=3600
USER_SESSION_TIMEOUT=3600
-AI_HOST=http://locahost:8001/api/
+AI_HOST=http://locahost:8585/api/
```
See Configuration section for more details.
diff --git a/build.gradle b/build.gradle
index af3a8da9..fb6b4a72 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,10 +1,10 @@
plugins {
id 'java'
- id 'org.springframework.boot' version '3.2.4'
- id 'io.spring.dependency-management' version '1.1.4'
+ id 'org.springframework.boot' version '3.3.3'
+ id 'io.spring.dependency-management' version '1.1.6'
id 'checkstyle'
id 'com.github.ben-manes.versions' version '0.51.0'
- id 'org.sonarqube' version '5.0.0.4638'
+ id 'org.sonarqube' version '5.1.0.4882'
id 'com.adarshr.test-logger' version '4.0.0'
id 'jacoco'
id 'idea'
@@ -44,19 +44,19 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
- implementation 'org.springframework.session:spring-session-jdbc:3.2.2'
- implementation 'org.flywaydb:flyway-core:10.10.0'
- implementation "org.flywaydb:flyway-database-postgresql:10.10.0"
+ implementation 'org.springframework.session:spring-session-jdbc:3.3.2'
+ implementation 'org.flywaydb:flyway-core:10.17.3'
+ implementation "org.flywaydb:flyway-database-postgresql:10.17.3"
implementation 'commons-lang:commons-lang:2.6'
implementation 'commons-beanutils:commons-beanutils:1.9.4'
- implementation 'com.github.erosb:json-sKema:0.14.0'
+ implementation 'com.github.erosb:json-sKema:0.16.0'
compileOnly 'org.projectlombok:lombok'
- runtimeOnly 'org.postgresql:postgresql:42.7.3'
+ runtimeOnly 'org.postgresql:postgresql:42.7.4'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
- testImplementation 'io.cucumber:cucumber-java:7.16.1'
- testImplementation 'io.cucumber:cucumber-junit:7.16.1'
- testImplementation 'org.junit.vintage:junit-vintage-engine:5.10.2'
+ testImplementation 'io.cucumber:cucumber-java:7.18.1'
+ testImplementation 'io.cucumber:cucumber-junit:7.18.1'
+ testImplementation 'org.junit.vintage:junit-vintage-engine:5.11.0'
}
tasks.named('test') {
diff --git a/changelog.md b/changelog.md
index cba8f6b1..2019cc53 100644
--- a/changelog.md
+++ b/changelog.md
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* `GET /api/user/me/roles`, to get its roles.
* `GET /api/user/me/groups`, to get its groups.
* `GET /api/user/me/scopes`, to get its scopes.
+ * `GET /api/users/me/ai/conversations`, to get its conversations.
* For all users:
* `GET /api/users`, to get all users.
* `GET /api/users/{login}`, to get user by login.
@@ -29,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* `GET /api/users/{login}/scopes`, to get all scopes associated to the user.
* `GET /api/users/{login}/permissions`, to get all permissions associated to the user.
* `GET /api/users/{login}/picture`, to get picture associated to the user.
+ * `GET /api/users/{login}/ai/conversations`, to get all conversations associated to the user.
* For all roles:
* `GET /api/roles`, to get all roles.
* `POST /api/roles`, to create a role.
@@ -90,6 +92,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* `GET /api/libraries/templates/[TEMPLATE_ID]/schemas/[INDEX]`, to get schema of template.
* `GET /api/libraries/templates/[TEMPLATE_ID]/files/[INDEX]`, to get file of template.
* For AI:
- * `GET /api/ia`, to create diagram with AI.
+ * `GET /api/ai/generate`, to generate diagram with AI.
+ * `GET /api/ai/conversations`, to get all conversation.
+ * `POST /api/ai/conversations`, to create a conversation.
+ * `GET /api/ai/conversations/[CONVERSATION_ID]`, to get a conversation by id.
+ * `DELETE /api/ai/conversations/[CONVERSATION_ID]`, to delete a conversation.
+ * `GET /api/ai/conversations/[CONVERSATION_ID]/messages`, to get all messages of a conversations.
+ * `POST /api/ai/conversations/[CONVERSATION_ID]/messages`, to send a message to AI.
* `/api/login`, to login.
* `/api/redirect`, to redirect with token on leto-modelizer/leto-modelizer-admin.
diff --git a/docker-compose-e2e.yml b/docker-compose-e2e.yml
index b58f5676..cb14e4af 100644
--- a/docker-compose-e2e.yml
+++ b/docker-compose-e2e.yml
@@ -10,7 +10,7 @@ services:
ai:
build: ./src/test/resources/ai
ports:
- - "8001:80"
+ - "8585:80"
volumes:
- ./src/test/resources/ai:/var/www/html
restart: always
@@ -29,7 +29,7 @@ services:
image: leto-modelizer-api:latest
environment:
DATABASE_HOST: db:26257
- AI_HOST: http://ai/api/
+ AI_HOST: http://ai/
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
LIBRARY_HOST_WHITELIST: http://libraries/
diff --git a/docker-compose.yml b/docker-compose.yml
index 32cf4c41..924ec474 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -28,7 +28,7 @@ services:
image: leto-modelizer-api:latest
environment:
DATABASE_HOST: db:26257
- AI_HOST: http://ai/api/
+ AI_HOST: http://ai/
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
LIBRARY_HOST_WHITELIST: http://libraries/
diff --git a/src/main/java/com/ditrit/letomodelizerapi/config/Constants.java b/src/main/java/com/ditrit/letomodelizerapi/config/Constants.java
index f988858c..f657c909 100644
--- a/src/main/java/com/ditrit/letomodelizerapi/config/Constants.java
+++ b/src/main/java/com/ditrit/letomodelizerapi/config/Constants.java
@@ -26,6 +26,16 @@ public final class Constants {
*/
public static final String DEFAULT_USER_PROPERTY = "login";
+ /**
+ * The constant representing the default context property.
+ */
+ public static final String DEFAULT_CONTEXT_PROPERTY = "context";
+
+ /**
+ * The constant representing the default update date property.
+ */
+ public static final String DEFAULT_UPDATE_DATE_PROPERTY = "updateDate";
+
/**
* Private constructor.
*/
diff --git a/src/main/java/com/ditrit/letomodelizerapi/controller/AIController.java b/src/main/java/com/ditrit/letomodelizerapi/controller/AIController.java
index b8a7a6d0..698acbf3 100644
--- a/src/main/java/com/ditrit/letomodelizerapi/controller/AIController.java
+++ b/src/main/java/com/ditrit/letomodelizerapi/controller/AIController.java
@@ -1,25 +1,48 @@
package com.ditrit.letomodelizerapi.controller;
-import com.ditrit.letomodelizerapi.model.ai.AIRequestRecord;
+
+import com.ditrit.letomodelizerapi.controller.model.QueryFilter;
+import com.ditrit.letomodelizerapi.model.BeanMapper;
+import com.ditrit.letomodelizerapi.model.ai.AIConversationDTO;
+import com.ditrit.letomodelizerapi.model.ai.AIConversationRecord;
+import com.ditrit.letomodelizerapi.model.ai.AICreateFileRecord;
+import com.ditrit.letomodelizerapi.model.ai.AIMessageDTO;
+import com.ditrit.letomodelizerapi.model.mapper.ai.AIMessageToDTOMapper;
+import com.ditrit.letomodelizerapi.model.permission.ActionPermission;
+import com.ditrit.letomodelizerapi.model.permission.EntityPermission;
import com.ditrit.letomodelizerapi.persistence.model.User;
import com.ditrit.letomodelizerapi.service.AIService;
+import com.ditrit.letomodelizerapi.service.UserPermissionService;
import com.ditrit.letomodelizerapi.service.UserService;
+import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import jakarta.ws.rs.BeanParam;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriInfo;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
+import java.io.IOException;
+import java.util.Map;
+import java.util.UUID;
+
/**
* Controller to manage ai endpoint.
*/
@@ -28,7 +51,7 @@
@Controller
@Slf4j
@AllArgsConstructor(onConstructor = @__(@Autowired))
-public class AIController {
+public class AIController implements DefaultController {
/**
* Service to manage user.
@@ -36,40 +59,231 @@ public class AIController {
private UserService userService;
/**
- * Service to manage ai request.
+ * Service to manage user permissions.
+ */
+ private UserPermissionService userPermissionService;
+
+ /**
+ * Service to manage AI request.
*/
private AIService aiService;
/**
- * Handles a POST request to initiate an interaction with an Artificial Intelligence (AI) based on the provided
+ * Handles a POST request to generate files with an Artificial Intelligence (AI) based on the provided
* request details.
- * This endpoint accepts an AI request record, which includes the parameters necessary for the AI interaction.
+ * This endpoint accepts an AI request record, which includes the parameters necessary for the files generation.
* The method retrieves the user from the session, logs the request details, and forwards the request to the AI
* service. It then constructs and returns a response containing the AI's output.
*
*
The method uses the AI service to process the request by the user, generating a JSON response that is returned
* to the client.
- * This process allows for dynamic interactions with the AI, facilitating use cases such as querying for
- * information, executing commands, or initiating workflows within the application.
*
* @param request the HttpServletRequest, used to access the user's session.
- * @param aiRequestRecord the request details for the AI, validated to ensure it meets the expected format.
- * @return a Response object containing the AI's response in JSON format, with a status of OK (200).
+ * @param aiCreateFileRecord the request details for the AI, validated to ensure it meets the expected format.
+ * @return a Response object containing the AI's response in JSON format, with a status of CREATED (201).
*/
-
@POST
- public Response requestAI(final @Context HttpServletRequest request,
- final @Valid AIRequestRecord aiRequestRecord) throws InterruptedException {
+ @Path("/generate")
+ public Response generateFiles(final @Context HttpServletRequest request,
+ final @Valid AICreateFileRecord aiCreateFileRecord) {
HttpSession session = request.getSession();
User user = userService.getFromSession(session);
- log.info("[{}] Received POST request to request AI with {}", user.getLogin(), aiRequestRecord);
+ log.info("[{}] Received POST request to generate files with AI with {}",
+ user.getLogin(), aiCreateFileRecord);
- String json = aiService.sendRequest(aiRequestRecord);
+ String json = aiService.createFile(aiCreateFileRecord);
return Response.status(HttpStatus.CREATED.value())
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.entity(json)
.build();
}
+
+ /**
+ * Handles a POST request to create a new AI conversation.
+ * This endpoint allows users to initiate a conversation with AI by providing the necessary details.
+ *
+ * @param request the HttpServletRequest used to access the user's session.
+ * @param aiConversationRecord the details of the AI conversation to be created, validated for correctness.
+ * @return a Response object containing the created AI conversation DTO with a status of CREATED (201).
+ * @throws JsonProcessingException if there is an error processing the request data.
+ */
+ @POST
+ @Path("/conversations")
+ public Response createConversations(final @Context HttpServletRequest request,
+ final @Valid AIConversationRecord aiConversationRecord)
+ throws JsonProcessingException {
+ HttpSession session = request.getSession();
+ User user = userService.getFromSession(session);
+
+ log.info("[{}] Received POST Received GET request to create AI conversation with {}",
+ user.getLogin(), aiConversationRecord);
+
+ AIConversationDTO dto = new BeanMapper<>(AIConversationDTO.class)
+ .apply(aiService.createConversation(user, aiConversationRecord));
+
+ return Response.status(HttpStatus.CREATED.value()).entity(dto).build();
+ }
+
+ /**
+ * Handles a GET request to retrieve all AI conversations with optional filtering and pagination.
+ * This endpoint allows administrators to view all AI conversations.
+ *
+ * @param request the HttpServletRequest used to access the user's session.
+ * @param uriInfo UriInfo object to retrieve query parameters.
+ * @param queryFilter the filter criteria and pagination information.
+ * @return a Response object containing a paginated list of AIConversationDTOs.
+ */
+ @GET
+ @Path("/conversations")
+ public Response findAllConversations(final @Context HttpServletRequest request,
+ final @Context UriInfo uriInfo,
+ final @BeanParam @Valid QueryFilter queryFilter) {
+ HttpSession session = request.getSession();
+ User user = userService.getFromSession(session);
+ userPermissionService.checkIsAdmin(user, null);
+ Map filters = this.getFilters(uriInfo);
+
+ log.info("[{}] Received GET request to get AI conversations with the following filters: {}",
+ user.getLogin(), filters);
+
+ Page resources = aiService.findAllConversations(filters, queryFilter.getPagination())
+ .map(new BeanMapper<>(AIConversationDTO.class));
+
+ return Response.status(getStatus(resources)).entity(resources).build();
+ }
+
+ /**
+ * Handles a GET request to retrieve a specific AI conversation by its ID.
+ * This endpoint allows users to view details of a specific conversation.
+ *
+ * @param request the HttpServletRequest used to access the user's session.
+ * @param id the ID of the AI conversation to retrieve.
+ * @return a Response object containing the AIConversationDTO with a status of OK (200).
+ */
+ @GET
+ @Path("/conversations/{id}")
+ public Response getConversationById(final @Context HttpServletRequest request,
+ final @PathParam("id") @Valid @NotNull UUID id) {
+ HttpSession session = request.getSession();
+ User user = userService.getFromSession(session);
+
+ log.info("[{}] Received GET request to get AI conversation with id {}", user.getLogin(), id);
+
+ AIConversationDTO dto = new BeanMapper<>(AIConversationDTO.class)
+ .apply(aiService.getConversationById(user, id));
+
+ return Response.status(HttpStatus.OK.value()).entity(dto).build();
+ }
+
+ /**
+ * Handles a PUT request to update an existing AI conversation by its ID.
+ * This endpoint allows users to modify the details of an existing conversation.
+ *
+ * @param request the HttpServletRequest used to access the user's session.
+ * @param id the ID of the AI conversation to update.
+ * @param aiConversationRecord the new details for the AI conversation, validated for correctness.
+ * @return a Response object containing the updated AIConversationDTO with a status of OK (200).
+ * @throws JsonProcessingException if there is an error processing the request data.
+ */
+ @PUT
+ @Path("/conversations/{id}")
+ public Response updateConversationById(final @Context HttpServletRequest request,
+ final @PathParam("id") @Valid @NotNull UUID id,
+ final @Valid AIConversationRecord aiConversationRecord)
+ throws JsonProcessingException {
+ HttpSession session = request.getSession();
+ User user = userService.getFromSession(session);
+
+ log.info("[{}] Received PUT request to update AI conversation with id {} with {}",
+ user.getLogin(), id, aiConversationRecord);
+
+ AIConversationDTO dto = new BeanMapper<>(AIConversationDTO.class)
+ .apply(aiService.updateConversationById(user, id, aiConversationRecord));
+
+ return Response.status(HttpStatus.OK.value()).entity(dto).build();
+ }
+
+ /**
+ * Handles a DELETE request to remove a specific AI conversation by its ID.
+ * This endpoint allows users to delete an AI conversation.
+ *
+ * @param request the HttpServletRequest used to access the user's session.
+ * @param id the ID of the AI conversation to delete.
+ * @return a Response object with a status of NO CONTENT (204) upon successful deletion.
+ */
+ @DELETE
+ @Path("/conversations/{id}")
+ public Response deleteConversationById(final @Context HttpServletRequest request,
+ final @PathParam("id") @Valid @NotNull UUID id) {
+ HttpSession session = request.getSession();
+ User user = userService.getFromSession(session);
+
+ log.info("[{}] Received GET request to get AI conversation with id {}", user.getLogin(), id);
+
+ if (userPermissionService.hasPermission(user, EntityPermission.ADMIN, ActionPermission.ACCESS)) {
+ aiService.deleteConversationById(id);
+ } else {
+ aiService.deleteConversationById(user, id);
+ }
+
+ return Response.noContent().build();
+ }
+
+ /**
+ * Handles a POST request to send a message to a specific AI conversation.
+ * This endpoint allows users to communicate with the AI by sending a message to an exinsting conversation.
+ *
+ * @param request the HttpServletRequest used to access the user's session.
+ * @param id the ID of the AI conversation to which the message is sent.
+ * @param message the content of the message to send to the AI.
+ * @return a Response object containing the AI's reply in plain text with a status of CREATED (201).
+ * @throws JsonProcessingException if there is an error processing the request data.
+ */
+ @POST
+ @Path("/conversations/{id}/messages")
+ public Response createConversationMessage(final @Context HttpServletRequest request,
+ final @PathParam("id") @Valid @NotNull UUID id,
+ final @Valid @NotNull String message) throws IOException {
+ HttpSession session = request.getSession();
+ User user = userService.getFromSession(session);
+
+ log.info("[{}] Received POST request to send message to conversation id {}",
+ user.getLogin(), id.toString());
+
+ AIMessageDTO aiMessageDTO = new AIMessageToDTOMapper().apply(aiService.sendMessage(user, id, message));
+
+ return Response.status(HttpStatus.CREATED.value()).entity(aiMessageDTO).build();
+ }
+
+ /**
+ * Handles a GET request to retrieve all messages from a specific AI conversation with optional filtering and
+ * pagination.
+ * This endpoint allows users to view the conversation history with AI.
+ *
+ * @param request the HttpServletRequest used to access the user's session.
+ * @param id the ID of the AI conversation from which to retrieve messages.
+ * @param uriInfo UriInfo object to retrieve query parameters.
+ * @param queryFilter the filter criteria and pagination information.
+ * @return a Response object containing a paginated list of AIMessageDTOs.
+ */
+ @GET
+ @Path("/conversations/{id}/messages")
+ public Response findAllMessages(final @Context HttpServletRequest request,
+ final @PathParam("id") @Valid @NotNull UUID id,
+ final @Context UriInfo uriInfo,
+ final @BeanParam @Valid QueryFilter queryFilter) {
+ HttpSession session = request.getSession();
+ User user = userService.getFromSession(session);
+ Map filters = this.getFilters(uriInfo);
+
+ log.info("[{}] Received GET request to get AI messages with conversation id {} with the following filters: {}",
+ user.getLogin(), id.toString(), filters);
+
+ Page resources = aiService.findAllMessages(user, id, filters, queryFilter.getPagination())
+ .map(new AIMessageToDTOMapper());
+
+ return Response.status(this.getStatus(resources)).entity(resources).build();
+ }
}
diff --git a/src/main/java/com/ditrit/letomodelizerapi/controller/CurrentUserController.java b/src/main/java/com/ditrit/letomodelizerapi/controller/CurrentUserController.java
index 2fb137b2..74712a2d 100644
--- a/src/main/java/com/ditrit/letomodelizerapi/controller/CurrentUserController.java
+++ b/src/main/java/com/ditrit/letomodelizerapi/controller/CurrentUserController.java
@@ -4,9 +4,11 @@
import com.ditrit.letomodelizerapi.model.BeanMapper;
import com.ditrit.letomodelizerapi.model.accesscontrol.AccessControlDTO;
import com.ditrit.letomodelizerapi.model.accesscontrol.AccessControlType;
+import com.ditrit.letomodelizerapi.model.ai.AIConversationDTO;
import com.ditrit.letomodelizerapi.model.permission.PermissionDTO;
import com.ditrit.letomodelizerapi.model.user.UserDTO;
import com.ditrit.letomodelizerapi.persistence.model.User;
+import com.ditrit.letomodelizerapi.service.AIService;
import com.ditrit.letomodelizerapi.service.AccessControlService;
import com.ditrit.letomodelizerapi.service.UserPermissionService;
import com.ditrit.letomodelizerapi.service.UserService;
@@ -53,6 +55,10 @@ public class CurrentUserController implements DefaultController {
* Service to manage access control.
*/
private AccessControlService accessControlService;
+ /**
+ * Service to manage AI request.
+ */
+ private AIService aiService;
/**
* The maximum age for caching user pictures.
@@ -73,6 +79,7 @@ public class CurrentUserController implements DefaultController {
* @param userService the service responsible for managing user details and operations.
* @param userPermissionService the service for checking and validating user permissions.
* @param accessControlService the service for managing access controls and security.
+ * @param aiService the service for managing AI request.
* @param userPictureCacheMaxAge the configured maximum age for caching user pictures, specified in application
* properties, indicating how long client browsers should cache user pictures before
* requesting them again.
@@ -81,10 +88,12 @@ public class CurrentUserController implements DefaultController {
public CurrentUserController(final UserService userService,
final UserPermissionService userPermissionService,
final AccessControlService accessControlService,
+ final AIService aiService,
final @Value("${user.picture.cache.max.age}") String userPictureCacheMaxAge) {
this.userService = userService;
this.userPermissionService = userPermissionService;
this.accessControlService = accessControlService;
+ this.aiService = aiService;
this.userPictureCacheMaxAge = userPictureCacheMaxAge;
}
@@ -258,4 +267,41 @@ public Response getMyScopes(final @Context HttpServletRequest request,
return Response.status(this.getStatus(resources)).entity(resources).build();
}
+
+ /**
+ * Retrieves all AI conversations associated to the current user.
+ * This endpoint provides a paginated list of AI conversations that the current user has, based on the provided
+ * query filters.
+ * The method uses the session information from the HttpServletRequest to identify the current user and
+ * then fetches their AI conversations using the AIService.
+ *
+ * @param request HttpServletRequest to access the HTTP session and thereby identify the current user.
+ * @param uriInfo UriInfo to extract query parameters for additional filtering.
+ * @param queryFilter BeanParam object for pagination and filtering purposes.
+ * @return a Response containing a paginated list of AIConversationDTO objects representing the AI conversation of
+ * the current user.
+ */
+ @GET
+ @Path("/ai/conversations")
+ public Response getMyAIConversations(final @Context HttpServletRequest request,
+ final @Context UriInfo uriInfo,
+ final @BeanParam @Valid QueryFilter queryFilter) {
+ HttpSession session = request.getSession();
+ User user = userService.getFromSession(session);
+ Map filters = this.getFilters(uriInfo);
+
+ log.info(
+ "[{}] Received GET request to get current user AI conversations with the following filters: {}",
+ user.getLogin(),
+ filters
+ );
+
+ Page resources = aiService.findAll(
+ user,
+ filters,
+ queryFilter.getPagination()
+ ).map(new BeanMapper<>(AIConversationDTO.class));
+
+ return Response.status(this.getStatus(resources)).entity(resources).build();
+ }
}
diff --git a/src/main/java/com/ditrit/letomodelizerapi/controller/UserController.java b/src/main/java/com/ditrit/letomodelizerapi/controller/UserController.java
index 971c8b1f..aac73aed 100644
--- a/src/main/java/com/ditrit/letomodelizerapi/controller/UserController.java
+++ b/src/main/java/com/ditrit/letomodelizerapi/controller/UserController.java
@@ -6,9 +6,11 @@
import com.ditrit.letomodelizerapi.model.accesscontrol.AccessControlDTO;
import com.ditrit.letomodelizerapi.model.accesscontrol.AccessControlDirectDTO;
import com.ditrit.letomodelizerapi.model.accesscontrol.AccessControlType;
+import com.ditrit.letomodelizerapi.model.ai.AIConversationDTO;
import com.ditrit.letomodelizerapi.model.permission.PermissionDTO;
import com.ditrit.letomodelizerapi.model.user.UserDTO;
import com.ditrit.letomodelizerapi.persistence.model.User;
+import com.ditrit.letomodelizerapi.service.AIService;
import com.ditrit.letomodelizerapi.service.AccessControlService;
import com.ditrit.letomodelizerapi.service.UserPermissionService;
import com.ditrit.letomodelizerapi.service.UserService;
@@ -58,6 +60,11 @@ public class UserController implements DefaultController {
*/
private AccessControlService accessControlService;
+ /**
+ * Service to manage access controls.
+ */
+ private AIService aiService;
+
/**
* The maximum age for caching user pictures.
* This value is typically specified in application properties and injected into the controller or service
@@ -74,16 +81,19 @@ public class UserController implements DefaultController {
* @param userService the service responsible for user-related operations.
* @param userPermissionService the service for checking user permissions.
* @param accessControlService the service for managing access controls.
+ * @param aiService the service for managing AI functionality.
* @param userPictureCacheMaxAge the maximum age for caching user pictures, injected from application properties.
*/
@Autowired
public UserController(final UserService userService,
final UserPermissionService userPermissionService,
final AccessControlService accessControlService,
+ final AIService aiService,
final @Value("${user.picture.cache.max.age}") String userPictureCacheMaxAge) {
this.userService = userService;
this.userPermissionService = userPermissionService;
this.accessControlService = accessControlService;
+ this.aiService = aiService;
this.userPictureCacheMaxAge = userPictureCacheMaxAge;
}
@@ -323,6 +333,50 @@ public Response getScopesOfUser(final @Context HttpServletRequest request,
return Response.status(this.getStatus(resources)).entity(resources).build();
}
+ /**
+ * Retrieves scopes associated with a user identified by their login.
+ *
+ * This method processes a GET request to fetch scopes assigned to a user, utilizing the user's login as the
+ * identifier. It supports filtering based on query parameters and pagination. The operation requires administrative
+ * privileges to execute, as it involves accessing sensitive user role information.
+ *
+ * @param request the HttpServletRequest from which to obtain the HttpSession for user validation.
+ * @param login the login identifier of the user whose scopes are being requested. Must be a valid, non-blank
+ * String.
+ * @param uriInfo UriInfo context to extract query parameters for filtering results.
+ * @param queryFilter bean parameter encapsulating filtering and pagination criteria.
+ * @return a Response object containing the requested page of AccessControlDTO objects representing the scopes
+ * associated with the specified user. The response status varies based on the outcome of the request.
+ */
+ @GET
+ @Path("/{login}/ai/conversations")
+ public Response getAIConversationsOfUser(final @Context HttpServletRequest request,
+ final @PathParam(Constants.DEFAULT_USER_PROPERTY) @Valid @NotBlank String login,
+ final @Context UriInfo uriInfo,
+ final @BeanParam @Valid QueryFilter queryFilter) {
+ HttpSession session = request.getSession();
+ User me = userService.getFromSession(session);
+
+ Map filters = this.getFilters(uriInfo);
+ log.info(
+ "[{}] Received GET request to get AI conversations of user {} with the following filters: {}",
+ me.getLogin(),
+ login,
+ filters
+ );
+
+ User user = userService.findByLogin(login);
+
+ if (!me.getId().equals(user.getId())) {
+ userPermissionService.checkIsAdmin(me, null);
+ }
+
+ Page resources = aiService.findAll(user, filters, queryFilter.getPagination())
+ .map(new BeanMapper<>(AIConversationDTO.class));
+
+ return Response.status(this.getStatus(resources)).entity(resources).build();
+ }
+
/**
* Retrieves the profile picture of a user identified by their login. Access to this endpoint is restricted to
* users with administrative privileges. The method fetches the picture from the user service and returns it
diff --git a/src/main/java/com/ditrit/letomodelizerapi/model/ai/AIConversationDTO.java b/src/main/java/com/ditrit/letomodelizerapi/model/ai/AIConversationDTO.java
new file mode 100644
index 00000000..14510e4b
--- /dev/null
+++ b/src/main/java/com/ditrit/letomodelizerapi/model/ai/AIConversationDTO.java
@@ -0,0 +1,40 @@
+package com.ditrit.letomodelizerapi.model.ai;
+
+import lombok.Data;
+
+import java.sql.Timestamp;
+import java.util.UUID;
+
+/**
+ * Data Transfer Object (DTO) for AIConversation.
+ * This class is used for transferring AIConversation data between different layers of the application,
+ * typically between services and controllers. It is designed to encapsulate the data attributes of an AIConversation
+ * entity in a form that is easy to serialize and deserialize when sending responses or requests.
+ */
+@Data
+public class AIConversationDTO {
+ /**
+ * The unique identifier of the AIConversation entity.
+ * This field represents the primary key in the database.
+ */
+ private UUID id;
+ /**
+ * The conversation key of the AIConversation entity.
+ */
+ private String key;
+
+ /**
+ * Checksum of last diagram files sent.
+ */
+ private String checksum;
+
+ /**
+ * Checksum of last diagram files sent.
+ */
+ private Long size;
+
+ /**
+ * The last update date of the conversation.
+ */
+ private Timestamp updateDate;
+}
diff --git a/src/main/java/com/ditrit/letomodelizerapi/model/ai/AIConversationRecord.java b/src/main/java/com/ditrit/letomodelizerapi/model/ai/AIConversationRecord.java
new file mode 100644
index 00000000..8e09e466
--- /dev/null
+++ b/src/main/java/com/ditrit/letomodelizerapi/model/ai/AIConversationRecord.java
@@ -0,0 +1,25 @@
+package com.ditrit.letomodelizerapi.model.ai;
+
+import com.ditrit.letomodelizerapi.model.file.FileRecord;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+import java.util.List;
+
+/**
+ * A record representing the data required to create a new conversation with AI.
+ *
+ * @param project the name of the project associated with the conversation. Cannot be blank.
+ * @param diagram the path to the diagram related to the conversation. Cannot be blank.
+ * @param plugin the name of the plugin involved in the conversation. Cannot be blank.
+ * @param checksum the checksum of the diagram files for ensuring data integrity. May be null.
+ * @param files a list of all diagram files associated with the conversation. Cannot be null.
+ */
+public record AIConversationRecord(
+ @NotBlank String project,
+ @NotBlank String diagram,
+ @NotBlank String plugin,
+ String checksum,
+ @NotNull List files
+) {
+}
diff --git a/src/main/java/com/ditrit/letomodelizerapi/model/ai/AIRequestRecord.java b/src/main/java/com/ditrit/letomodelizerapi/model/ai/AICreateFileRecord.java
similarity index 96%
rename from src/main/java/com/ditrit/letomodelizerapi/model/ai/AIRequestRecord.java
rename to src/main/java/com/ditrit/letomodelizerapi/model/ai/AICreateFileRecord.java
index aee3cd9d..7ba81ef6 100644
--- a/src/main/java/com/ditrit/letomodelizerapi/model/ai/AIRequestRecord.java
+++ b/src/main/java/com/ditrit/letomodelizerapi/model/ai/AICreateFileRecord.java
@@ -14,7 +14,7 @@
* to ensure that only valid request types are considered.
* @param description a description of the AI request, providing context or additional information. Must not be blank.
*/
-public record AIRequestRecord(
+public record AICreateFileRecord(
@NotBlank String plugin,
@Pattern(regexp = "diagram") String type,
@NotBlank String description
diff --git a/src/main/java/com/ditrit/letomodelizerapi/model/ai/AIMessageDTO.java b/src/main/java/com/ditrit/letomodelizerapi/model/ai/AIMessageDTO.java
new file mode 100644
index 00000000..6fee493c
--- /dev/null
+++ b/src/main/java/com/ditrit/letomodelizerapi/model/ai/AIMessageDTO.java
@@ -0,0 +1,40 @@
+package com.ditrit.letomodelizerapi.model.ai;
+
+import lombok.Data;
+
+import java.sql.Timestamp;
+import java.util.UUID;
+
+/**
+ * Data Transfer Object (DTO) for AIMessage.
+ * This class is used for transferring AIMessage data between different layers of the application,
+ * typically between services and controllers. It is designed to encapsulate the data attributes of an AIMessage
+ * entity in a form that is easy to serialize and deserialize when sending responses or requests.
+ */
+@Data
+public class AIMessageDTO {
+ /**
+ * The unique identifier of the AIConversation entity.
+ * This field represents the primary key in the database.
+ */
+ private UUID id;
+ /**
+ * The conversation uuid of the AIConversation entity.
+ */
+ private UUID aiConversation;
+
+ /**
+ * Indicate the author of the message: if true, the message is from the user; otherwise, it is from the AI.
+ */
+ private Boolean isUser;
+
+ /**
+ * The message.
+ */
+ private String message;
+
+ /**
+ * The last update date of the message.
+ */
+ private Timestamp updateDate;
+}
diff --git a/src/main/java/com/ditrit/letomodelizerapi/model/file/FileRecord.java b/src/main/java/com/ditrit/letomodelizerapi/model/file/FileRecord.java
new file mode 100644
index 00000000..21ae387e
--- /dev/null
+++ b/src/main/java/com/ditrit/letomodelizerapi/model/file/FileRecord.java
@@ -0,0 +1,15 @@
+package com.ditrit.letomodelizerapi.model.file;
+
+import jakarta.validation.constraints.NotBlank;
+
+/**
+ * A record representing a file.
+ *
+ * @param path the file path. Cannot be blank.
+ * @param content the content of the file. Cannot be blank.
+ */
+public record FileRecord(
+ @NotBlank String path,
+ @NotBlank String content
+) {
+}
diff --git a/src/main/java/com/ditrit/letomodelizerapi/model/file/package-info.java b/src/main/java/com/ditrit/letomodelizerapi/model/file/package-info.java
new file mode 100644
index 00000000..cef2ba92
--- /dev/null
+++ b/src/main/java/com/ditrit/letomodelizerapi/model/file/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Package containing all models related to File.
+ */
+package com.ditrit.letomodelizerapi.model.file;
diff --git a/src/main/java/com/ditrit/letomodelizerapi/model/mapper/ai/AIMessageToDTOMapper.java b/src/main/java/com/ditrit/letomodelizerapi/model/mapper/ai/AIMessageToDTOMapper.java
new file mode 100644
index 00000000..203498a2
--- /dev/null
+++ b/src/main/java/com/ditrit/letomodelizerapi/model/mapper/ai/AIMessageToDTOMapper.java
@@ -0,0 +1,19 @@
+package com.ditrit.letomodelizerapi.model.mapper.ai;
+
+import com.ditrit.letomodelizerapi.model.BeanMapper;
+import com.ditrit.letomodelizerapi.model.ai.AIMessageDTO;
+import com.ditrit.letomodelizerapi.persistence.model.AIMessage;
+
+import java.util.Base64;
+import java.util.function.Function;
+
+public class AIMessageToDTOMapper implements Function {
+ @Override
+ public AIMessageDTO apply(final AIMessage aiMessage) {
+ AIMessageDTO aiMessageDTO = new BeanMapper<>(AIMessageDTO.class, "message").apply(aiMessage);
+
+ aiMessageDTO.setMessage(Base64.getEncoder().encodeToString(aiMessage.getMessage()));
+
+ return aiMessageDTO;
+ }
+}
diff --git a/src/main/java/com/ditrit/letomodelizerapi/model/mapper/ai/package-info.java b/src/main/java/com/ditrit/letomodelizerapi/model/mapper/ai/package-info.java
new file mode 100644
index 00000000..d96051d5
--- /dev/null
+++ b/src/main/java/com/ditrit/letomodelizerapi/model/mapper/ai/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Package that contains all mapper of AI models.
+ */
+package com.ditrit.letomodelizerapi.model.mapper.ai;
diff --git a/src/main/java/com/ditrit/letomodelizerapi/model/mapper/package-info.java b/src/main/java/com/ditrit/letomodelizerapi/model/mapper/package-info.java
new file mode 100644
index 00000000..2c04022c
--- /dev/null
+++ b/src/main/java/com/ditrit/letomodelizerapi/model/mapper/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Package that contains all mapper of models.
+ */
+package com.ditrit.letomodelizerapi.model.mapper;
diff --git a/src/main/java/com/ditrit/letomodelizerapi/persistence/model/AIConversation.java b/src/main/java/com/ditrit/letomodelizerapi/persistence/model/AIConversation.java
new file mode 100644
index 00000000..2a443d7e
--- /dev/null
+++ b/src/main/java/com/ditrit/letomodelizerapi/persistence/model/AIConversation.java
@@ -0,0 +1,78 @@
+package com.ditrit.letomodelizerapi.persistence.model;
+
+import com.ditrit.letomodelizerapi.persistence.specification.filter.FilterType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.PrePersist;
+import jakarta.persistence.Table;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+/**
+ * Entity representing an AI conversation associated with a user.
+ * This class maps to the 'ai_conversations' table in the database, storing AI conversations that are used
+ * to register all conversation with AI for a user.
+ */
+@Entity
+@Table(name = "ai_conversations")
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class AIConversation extends AbstractEntity {
+
+ /**
+ * Internal id.
+ */
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "aic_id")
+ @FilterType(type = FilterType.Type.UUID)
+ private UUID id;
+
+ /**
+ * The login identifier of the user to whom the conversation is associated.
+ */
+ @Column(name = "usr_id")
+ @FilterType(type = FilterType.Type.UUID)
+ private UUID userId;
+
+ /**
+ * The conversation key.
+ */
+ @Column(name = "key")
+ @FilterType(type = FilterType.Type.TEXT)
+ private String key;
+
+ /**
+ * Checksum of last diagram files sent.
+ */
+ @Column(name = "checksum")
+ private String checksum;
+
+ /**
+ * The conversation context for AI.
+ */
+ @Column(name = "context")
+ private String context;
+
+ /**
+ * Size of all conversation messages.
+ */
+ @Column(name = "size")
+ @FilterType(type = FilterType.Type.NUMBER)
+ private Long size;
+
+ /**
+ * Set insertDate before persisting in repository.
+ */
+ @PrePersist
+ public void prePersist() {
+ this.setInsertDate(Timestamp.valueOf(LocalDateTime.now()));
+ }
+}
diff --git a/src/main/java/com/ditrit/letomodelizerapi/persistence/model/AIMessage.java b/src/main/java/com/ditrit/letomodelizerapi/persistence/model/AIMessage.java
new file mode 100644
index 00000000..fe1a231b
--- /dev/null
+++ b/src/main/java/com/ditrit/letomodelizerapi/persistence/model/AIMessage.java
@@ -0,0 +1,63 @@
+package com.ditrit.letomodelizerapi.persistence.model;
+
+import com.ditrit.letomodelizerapi.persistence.specification.filter.FilterType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.PrePersist;
+import jakarta.persistence.Table;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+/**
+ * Entity representing an AI message associated with an AI conversation.
+ * This class maps to the 'ai_messages' table in the database, storing message between AI and user.
+ */
+@Entity
+@Table(name = "ai_messages")
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class AIMessage extends AbstractEntity {
+
+ /**
+ * Internal id.
+ */
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "aim_id")
+ @FilterType(type = FilterType.Type.UUID)
+ private UUID id;
+
+ /**
+ * The identifier of the conversation.
+ */
+ @Column(name = "aic_id")
+ @FilterType(type = FilterType.Type.UUID)
+ private UUID aiConversation;
+
+ /**
+ * Indicate the author of the message: if true, the message is from the user; otherwise, it is from the AI.
+ */
+ @Column(name = "is_user")
+ private Boolean isUser;
+
+ /**
+ * Base64 of compressed message.
+ */
+ @Column(name = "message")
+ private byte[] message;
+
+ /**
+ * Set insertDate before persisting in repository.
+ */
+ @PrePersist
+ public void prePersist() {
+ this.setInsertDate(Timestamp.valueOf(LocalDateTime.now()));
+ }
+}
diff --git a/src/main/java/com/ditrit/letomodelizerapi/persistence/model/AbstractEntity.java b/src/main/java/com/ditrit/letomodelizerapi/persistence/model/AbstractEntity.java
index 69dec3b6..d5cfbdd5 100644
--- a/src/main/java/com/ditrit/letomodelizerapi/persistence/model/AbstractEntity.java
+++ b/src/main/java/com/ditrit/letomodelizerapi/persistence/model/AbstractEntity.java
@@ -1,6 +1,7 @@
package com.ditrit.letomodelizerapi.persistence.model;
+import com.ditrit.letomodelizerapi.persistence.specification.filter.FilterType;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Version;
@@ -19,12 +20,14 @@ public class AbstractEntity {
* The creation date of this entity.
*/
@Column(name = "insert_date", updatable = false)
+ @FilterType(type = FilterType.Type.DATE)
private Timestamp insertDate;
/**
* The last update date of this entity.
*/
@Column(name = "update_date")
+ @FilterType(type = FilterType.Type.DATE)
@Version
private Timestamp updateDate;
}
diff --git a/src/main/java/com/ditrit/letomodelizerapi/persistence/repository/AIConversationRepository.java b/src/main/java/com/ditrit/letomodelizerapi/persistence/repository/AIConversationRepository.java
new file mode 100644
index 00000000..5216cfd2
--- /dev/null
+++ b/src/main/java/com/ditrit/letomodelizerapi/persistence/repository/AIConversationRepository.java
@@ -0,0 +1,43 @@
+package com.ditrit.letomodelizerapi.persistence.repository;
+
+import com.ditrit.letomodelizerapi.persistence.model.AIConversation;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Spring Data JPA repository interface for AIConversation entities.
+ * This interface extends JpaRepository, providing standard CRUD operations for AIConversation entities.
+ */
+public interface AIConversationRepository extends JpaRepository {
+ /**
+ * Checks if a AIConversation with the specified user Id and key already exists in the database.
+ *
+ * @param userId the userId to check for existence
+ * @param key the key to check for existence
+ * @return true if a AIConversation with the user Id and key exists, false otherwise
+ */
+ boolean existsByUserIdAndKey(UUID userId, String key);
+
+ /**
+ * Finds an AIConversation by its id and userId.
+ *
+ * @param id the id of the AIConversation.
+ * @param userId the userId associated with the AIConversation.
+ * @return an Optional containing the AIConversation if found, or an empty Optional if not found.
+ */
+ Optional findByIdAndUserId(UUID id, UUID userId);
+
+ /**
+ * Finds all AIConversations that match the provided specification, with pagination support.
+ *
+ * @param specification the specification to filter AIConversations.
+ * @param pageable the pagination information.
+ * @return a Page containing the AIConversations that match the specification.
+ */
+ Page findAll(Specification specification, Pageable pageable);
+}
diff --git a/src/main/java/com/ditrit/letomodelizerapi/persistence/repository/AIMessageRepository.java b/src/main/java/com/ditrit/letomodelizerapi/persistence/repository/AIMessageRepository.java
new file mode 100644
index 00000000..ebf49b02
--- /dev/null
+++ b/src/main/java/com/ditrit/letomodelizerapi/persistence/repository/AIMessageRepository.java
@@ -0,0 +1,24 @@
+package com.ditrit.letomodelizerapi.persistence.repository;
+
+import com.ditrit.letomodelizerapi.persistence.model.AIMessage;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.UUID;
+
+/**
+ * Spring Data JPA repository interface for AIMessage entities.
+ * This interface extends JpaRepository, providing standard CRUD operations for AIMessage entities.
+ */
+public interface AIMessageRepository extends JpaRepository {
+ /**
+ * Finds all AIMessages that match the provided specification, with pagination support.
+ *
+ * @param specification the specification to filter AIMessages.
+ * @param pageable the pagination information.
+ * @return a Page containing the AIMessages that match the specification.
+ */
+ Page findAll(Specification specification, Pageable pageable);
+}
diff --git a/src/main/java/com/ditrit/letomodelizerapi/persistence/specification/filter/DatePredicateFilter.java b/src/main/java/com/ditrit/letomodelizerapi/persistence/specification/filter/DatePredicateFilter.java
index 4fe388d5..942a8edf 100644
--- a/src/main/java/com/ditrit/letomodelizerapi/persistence/specification/filter/DatePredicateFilter.java
+++ b/src/main/java/com/ditrit/letomodelizerapi/persistence/specification/filter/DatePredicateFilter.java
@@ -21,11 +21,11 @@ public class DatePredicateFilter extends PredicateFilter {
/**
* Default date format for date filter.
*/
- public static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
+ public static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
/**
* Regex pattern for date format.
*/
- private static final String FILTER_SUB_PATTERN = "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}";
+ private static final String FILTER_SUB_PATTERN = "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}";
/**
* Regex to extract date.
*
diff --git a/src/main/java/com/ditrit/letomodelizerapi/service/AIService.java b/src/main/java/com/ditrit/letomodelizerapi/service/AIService.java
index ca1f1143..7f3cf416 100644
--- a/src/main/java/com/ditrit/letomodelizerapi/service/AIService.java
+++ b/src/main/java/com/ditrit/letomodelizerapi/service/AIService.java
@@ -1,11 +1,21 @@
package com.ditrit.letomodelizerapi.service;
-import com.ditrit.letomodelizerapi.model.ai.AIRequestRecord;
+import com.ditrit.letomodelizerapi.model.ai.AIConversationRecord;
+import com.ditrit.letomodelizerapi.model.ai.AICreateFileRecord;
+import com.ditrit.letomodelizerapi.persistence.model.AIConversation;
+import com.ditrit.letomodelizerapi.persistence.model.AIMessage;
+import com.ditrit.letomodelizerapi.persistence.model.User;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.UUID;
/**
* Service implementation for interacting with an Artificial Intelligence (AI).
- * This class provides concrete implementation for sending requests to an AI, based on user input and specific
- * request details encapsulated in an AIRequestRecord.
+ * This class provides concrete implementation for sending requests to an AI and based on user input.
*/
public interface AIService {
@@ -16,5 +26,95 @@ public interface AIService {
* @param aiRequest the AIRequestRecord containing details about the request to be sent to the AI.
* @return the response from the AI as a String.
*/
- String sendRequest(AIRequestRecord aiRequest);
+ String createFile(AICreateFileRecord aiRequest);
+
+ /**
+ * Creates a new AI conversation based on the provided user and conversation details.
+ *
+ * @param user the user initiating the conversation.
+ * @param aiConversationRecord an instance of AIConversationRecord containing the details of the conversation.
+ * @return the newly created AIConversation.
+ * @throws JsonProcessingException if there is an error processing the JSON data.
+ */
+ AIConversation createConversation(User user, AIConversationRecord aiConversationRecord)
+ throws JsonProcessingException;
+
+ /**
+ * Retrieves an AI conversation by its ID for the specified user.
+ *
+ * @param user the user requesting the conversation.
+ * @param id the ID of the conversation to retrieve.
+ * @return the AIConversation with the specified ID.
+ */
+ AIConversation getConversationById(User user, UUID id);
+
+ /**
+ * Updates an existing AI conversation by its ID for the specified user.
+ *
+ * @param user the user requesting the update.
+ * @param id the ID of the conversation to update.
+ * @param aiConversationRecord an instance of AIConversationRecord containing the updated conversation details.
+ * @return the updated AIConversation.
+ * @throws JsonProcessingException if there is an error processing the JSON data.
+ */
+ AIConversation updateConversationById(User user, UUID id, AIConversationRecord aiConversationRecord)
+ throws JsonProcessingException;
+
+ /**
+ * Deletes an AI conversation by its ID.
+ *
+ * @param id the ID of the conversation to delete.
+ */
+ void deleteConversationById(UUID id);
+
+ /**
+ * Deletes an AI conversation by its ID for the specified user.
+ *
+ * @param user the user requesting the deletion.
+ * @param id the ID of the conversation to delete.
+ */
+ void deleteConversationById(User user, UUID id);
+
+ /**
+ * Sends a message to an AI conversation.
+ *
+ * @param user the user sending the message.
+ * @param id the ID of the conversation to which the message is sent.
+ * @param message the content of the message to be sent.
+ * @return the AI's response to the message.
+ * @throws JsonProcessingException if there is an error processing the JSON data.
+ */
+ AIMessage sendMessage(User user, UUID id, String message) throws IOException;
+
+ /**
+ * Finds all AI conversations for a user with optional filtering and pagination.
+ *
+ * @param user the user for whom to find conversations.
+ * @param filters a map of filters to apply to the search.
+ * @param pageable pagination information.
+ * @return a Page containing the AI conversations matching the filters.
+ */
+ Page findAll(User user, Map filters, Pageable pageable);
+
+ /**
+ * Finds all messages for a specific AI conversation with optional filtering and pagination.
+ *
+ * @param user the user requesting the messages.
+ * @param id the ID of the conversation for which to find messages.
+ * @param filters a map of filters to apply to the search.
+ * @param pageable pagination information.
+ * @return a Page containing the messages for the specified conversation.
+ */
+ Page findAllMessages(User user, UUID id, Map filters, Pageable pageable);
+
+ /**
+ * Finds all AI conversations with optional filtering and pagination.
+ *
+ * @param immutableFilters a map of immutable filters to apply to the search.
+ * @param pageable pagination information.
+ * @return a Page containing all AI conversations matching the filters.
+ */
+ Page findAllConversations(Map immutableFilters,
+ Pageable pageable);
+
}
diff --git a/src/main/java/com/ditrit/letomodelizerapi/service/AIServiceImpl.java b/src/main/java/com/ditrit/letomodelizerapi/service/AIServiceImpl.java
index a0b538a9..adc96810 100644
--- a/src/main/java/com/ditrit/letomodelizerapi/service/AIServiceImpl.java
+++ b/src/main/java/com/ditrit/letomodelizerapi/service/AIServiceImpl.java
@@ -1,24 +1,49 @@
package com.ditrit.letomodelizerapi.service;
+import com.ditrit.letomodelizerapi.config.Constants;
+import com.ditrit.letomodelizerapi.model.ai.AIConversationRecord;
+import com.ditrit.letomodelizerapi.model.ai.AICreateFileRecord;
import com.ditrit.letomodelizerapi.model.error.ApiException;
import com.ditrit.letomodelizerapi.model.error.ErrorType;
-import com.ditrit.letomodelizerapi.model.ai.AIRequestRecord;
+import com.ditrit.letomodelizerapi.model.file.FileRecord;
+import com.ditrit.letomodelizerapi.persistence.model.AIConversation;
+import com.ditrit.letomodelizerapi.persistence.model.AIMessage;
+import com.ditrit.letomodelizerapi.persistence.model.User;
+import com.ditrit.letomodelizerapi.persistence.repository.AIConversationRepository;
+import com.ditrit.letomodelizerapi.persistence.repository.AIMessageRepository;
+import com.ditrit.letomodelizerapi.persistence.specification.SpecificationHelper;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.transaction.Transactional;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.zip.GZIPOutputStream;
/**
* Service implementation for interacting with an Artificial Intelligence (AI).
@@ -27,6 +52,7 @@
*/
@Slf4j
@Service
+@Transactional
public class AIServiceImpl implements AIService {
/**
@@ -34,6 +60,20 @@ public class AIServiceImpl implements AIService {
*/
private final String aiHost;
+ /**
+ * The AIConversationRepository instance is injected by Spring's dependency injection mechanism.
+ * This repository is used for performing database operations related to AIConversation entities, such as querying,
+ * saving and updating user data.
+ */
+ private final AIConversationRepository aiConversationRepository;
+
+ /**
+ * The AIConversationRepository instance is injected by Spring's dependency injection mechanism.
+ * This repository is used for performing database operations related to AIConversation entities, such as querying,
+ * saving and updating user data.
+ */
+ private final AIMessageRepository aiMessageRepository;
+
/**
* Constructor for AIServiceImpl.
* Initializes the service with the host address of the Artificial Intelligence (AI) system. This address is used to
@@ -42,26 +82,31 @@ public class AIServiceImpl implements AIService {
* The host address is injected from the application's configuration properties, allowing for flexible deployment
* and configuration of the AI service endpoint.
*
+ * @param aiConversationRepository Repository to manage AIConversation.
+ * @param aiMessageRepository Repository to manage AIMessage.
* @param aiHost the host address of the Artificial Intelligence system, injected from application properties.
*/
@Autowired
- public AIServiceImpl(@Value("${ai.host}") final String aiHost) {
+ public AIServiceImpl(final AIConversationRepository aiConversationRepository,
+ final AIMessageRepository aiMessageRepository,
+ @Value("${ai.host}") final String aiHost) {
+ this.aiConversationRepository = aiConversationRepository;
+ this.aiMessageRepository = aiMessageRepository;
this.aiHost = aiHost;
}
- @Override
- public String sendRequest(final AIRequestRecord aiRequest) {
- String url = String.format("%s%s/", aiHost, aiRequest.type());
-
- ObjectNode json = JsonNodeFactory.instance.objectNode();
- json.put("pluginName", aiRequest.plugin());
- json.put("description", aiRequest.description());
-
- String body = json.toString();
-
+ /**
+ * Sends a request to the AI service with the specified endpoint and request body.
+ *
+ * @param endpoint the URL of the AI endpoint to which the request is sent.
+ * @param body the content to be sent in the body of the request.
+ * @return the response body returned by the AI service.
+ */
+ public String sendRequest(final String endpoint, final String body) {
try {
+ URI uri = new URI(aiHost).resolve("api/").resolve(endpoint);
HttpRequest request = HttpRequest.newBuilder()
- .uri(new URI(url))
+ .uri(uri)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.headers(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
.version(HttpClient.Version.HTTP_1_1)
@@ -78,16 +123,217 @@ public String sendRequest(final AIRequestRecord aiRequest) {
}
if (!HttpStatus.valueOf(response.statusCode()).is2xxSuccessful()) {
- throw new ApiException(ErrorType.WRONG_VALUE, "url", url);
+ throw new ApiException(ErrorType.WRONG_VALUE, "url", uri.toString());
}
return response.body();
} catch (URISyntaxException | IOException e) {
- throw new ApiException(ErrorType.WRONG_VALUE, "url", url);
+ throw new ApiException(ErrorType.WRONG_VALUE, "url", aiHost + "api/" + endpoint);
} catch (InterruptedException e) {
- log.warn("InterruptedException during requesting ai with {} and ", url, body, e);
+ log.warn("InterruptedException during requesting ai with {} and {}", aiHost + "api/" + endpoint, body, e);
Thread.currentThread().interrupt();
- throw new ApiException(ErrorType.INTERNAL_ERROR, "url", url);
+ throw new ApiException(ErrorType.INTERNAL_ERROR, "url", aiHost + "api/" + endpoint);
+ }
+ }
+
+ /**
+ * Sends files to the AI service with the specified conversation.
+ *
+ * @param conversation The conversation.
+ * @param files Files to send.
+ * @return The context returned by the AI response.
+ */
+ public String sendFiles(final AIConversation conversation, final List files)
+ throws JsonProcessingException {
+ ObjectMapper mapper = new ObjectMapper();
+ ArrayNode arrayNode = mapper.createArrayNode();
+
+ files.forEach(file -> {
+ ObjectNode fileNode = mapper.createObjectNode();
+
+ fileNode.put("path", file.path());
+ fileNode.put("content", file.content());
+
+ arrayNode.add(fileNode);
+ });
+
+ ObjectNode json = JsonNodeFactory.instance.objectNode();
+ json.put("pluginName", conversation.getKey().split("/")[2]);
+ json.set("files", arrayNode);
+
+ JsonNode response = mapper.readTree(sendRequest("chat", json.toString()));
+
+ return response.get(Constants.DEFAULT_CONTEXT_PROPERTY).asText();
+ }
+
+ /**
+ * Compress message with GZIP.
+ * @param message Message to compress.
+ * @return Compressed message.
+ * @throws IOException If an I/O error has occurred.
+ */
+ public byte[] compress(final String message) throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ GZIPOutputStream gzip = new GZIPOutputStream(outputStream);
+
+ gzip.write(message.getBytes(StandardCharsets.UTF_8));
+ gzip.close();
+
+ return outputStream.toByteArray();
+ }
+
+ @Override
+ public String createFile(final AICreateFileRecord createFileRecord) {
+ ObjectNode json = JsonNodeFactory.instance.objectNode();
+ json.put("pluginName", createFileRecord.plugin());
+ json.put("description", createFileRecord.description());
+
+ return sendRequest(createFileRecord.type(), json.toString());
+ }
+
+ @Override
+ public AIConversation createConversation(final User user, final AIConversationRecord aiConversationRecord)
+ throws JsonProcessingException {
+ String key = String.format("%s/%s/%s",
+ aiConversationRecord.project(), aiConversationRecord.diagram(), aiConversationRecord.plugin());
+ if (aiConversationRepository.existsByUserIdAndKey(user.getId(), key)) {
+ throw new ApiException(ErrorType.ENTITY_ALREADY_EXISTS, "key", key);
}
+
+ AIConversation conversation = new AIConversation();
+ conversation.setUserId(user.getId());
+ conversation.setKey(key);
+ conversation.setChecksum(aiConversationRecord.checksum());
+ conversation.setSize(0L);
+
+ String context = sendFiles(conversation, aiConversationRecord.files());
+
+ conversation.setContext(context);
+
+ return aiConversationRepository.save(conversation);
+ }
+
+ @Override
+ public AIConversation getConversationById(final User user, final UUID id) {
+ return aiConversationRepository.findByIdAndUserId(id, user.getId())
+ .orElseThrow(() -> new ApiException(ErrorType.ENTITY_NOT_FOUND, "id", id.toString()));
+ }
+
+ @Override
+ public AIConversation updateConversationById(final User user,
+ final UUID id,
+ final AIConversationRecord aiConversationRecord)
+ throws JsonProcessingException {
+ AIConversation conversation = aiConversationRepository.findByIdAndUserId(id, user.getId())
+ .orElseThrow(() -> new ApiException(ErrorType.ENTITY_NOT_FOUND, "id", id.toString()));
+
+ String context = sendFiles(conversation, aiConversationRecord.files());
+
+ conversation.setChecksum(aiConversationRecord.checksum());
+ conversation.setContext(context);
+
+ return aiConversationRepository.save(conversation);
+ }
+
+ @Override
+ public void deleteConversationById(final UUID id) {
+ AIConversation conversation = aiConversationRepository.findById(id)
+ .orElseThrow(() -> new ApiException(ErrorType.ENTITY_NOT_FOUND, "id", id.toString()));
+
+ aiConversationRepository.delete(conversation);
+ }
+
+ @Override
+ public void deleteConversationById(final User user, final UUID id) {
+ AIConversation conversation = aiConversationRepository.findByIdAndUserId(id, user.getId())
+ .orElseThrow(() -> new ApiException(ErrorType.ENTITY_NOT_FOUND, "id", id.toString()));
+
+ aiConversationRepository.delete(conversation);
+ }
+
+ @Override
+ public AIMessage sendMessage(final User user, final UUID id, final String message) throws IOException {
+ AIConversation conversation = aiConversationRepository.findByIdAndUserId(id, user.getId())
+ .orElseThrow(() -> new ApiException(ErrorType.ENTITY_NOT_FOUND, "id", id.toString()));
+
+ byte[] compressedMessage = compress(message);
+ Long size = conversation.getSize() + compressedMessage.length;
+
+ AIMessage userMessage = new AIMessage();
+ userMessage.setAiConversation(conversation.getId());
+ userMessage.setIsUser(true);
+ userMessage.setMessage(compressedMessage);
+ aiMessageRepository.save(userMessage);
+
+ ObjectNode json = JsonNodeFactory.instance.objectNode();
+ json.put(Constants.DEFAULT_CONTEXT_PROPERTY, conversation.getContext());
+ json.put("message", message);
+
+ JsonNode response = new ObjectMapper().readTree(sendRequest("chat", json.toString()));
+
+ compressedMessage = compress(response.get("message").asText());
+ size += compressedMessage.length;
+
+ AIMessage aiMessage = new AIMessage();
+ aiMessage.setAiConversation(conversation.getId());
+ aiMessage.setIsUser(false);
+ aiMessage.setMessage(compressedMessage);
+ aiMessage = aiMessageRepository.save((aiMessage));
+
+ conversation.setContext(response.get(Constants.DEFAULT_CONTEXT_PROPERTY).asText());
+ conversation.setSize(size);
+ aiConversationRepository.save(conversation);
+
+ return aiMessage;
+ }
+
+ @Override
+ public Page findAll(final User user,
+ final Map immutableFilters,
+ final Pageable pageable) {
+ Map filters = new HashMap<>(immutableFilters);
+ filters.put("userId", user.getId().toString());
+
+ return aiConversationRepository.findAll(
+ new SpecificationHelper<>(AIConversation.class, filters),
+ PageRequest.of(
+ pageable.getPageNumber(),
+ pageable.getPageSize(),
+ pageable.getSortOr(Sort.by(Sort.Direction.DESC, Constants.DEFAULT_UPDATE_DATE_PROPERTY))
+ )
+ );
+ }
+
+ @Override
+ public Page findAllMessages(final User user,
+ final UUID id,
+ final Map immutableFilters,
+ final Pageable pageable) {
+ AIConversation conversation = aiConversationRepository.findByIdAndUserId(id, user.getId())
+ .orElseThrow(() -> new ApiException(ErrorType.ENTITY_NOT_FOUND, "id", id.toString()));
+
+ Map filters = new HashMap<>(immutableFilters);
+ filters.put("aiConversation", conversation.getId().toString());
+
+ return aiMessageRepository.findAll(
+ new SpecificationHelper<>(AIMessage.class, filters),
+ PageRequest.of(
+ pageable.getPageNumber(),
+ pageable.getPageSize(),
+ pageable.getSortOr(Sort.by(Sort.Direction.DESC, Constants.DEFAULT_UPDATE_DATE_PROPERTY)))
+ );
+ }
+
+ @Override
+ public Page findAllConversations(final Map immutableFilters,
+ final Pageable pageable) {
+
+ return aiConversationRepository.findAll(
+ new SpecificationHelper<>(AIConversation.class, immutableFilters),
+ PageRequest.of(
+ pageable.getPageNumber(),
+ pageable.getPageSize(),
+ pageable.getSortOr(Sort.by(Sort.Direction.DESC, Constants.DEFAULT_UPDATE_DATE_PROPERTY))
+ ));
}
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index ce1078b4..0de409cb 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -6,6 +6,7 @@ spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto=none
+spring.jpa.properties.hibernate.jdbc.time_zone=UTC
spring.flyway.baseline-on-migrate=true
spring.flyway.out-of-order=true
@@ -41,4 +42,4 @@ library.files.cache.max.age=${LIBRARY_FILES_CACHE_MAX_AGE:86400}
user.picture.cache.max.age=${USER_PICTURE_CACHE_MAX_AGE:604800}
csrf.token.timeout=${CSRF_TOKEN_TIMEOUT:3600}
server.servlet.session.timeout=${USER_SESSION_TIMEOUT:3600}
-ai.host=${AI_HOST}
+ai.host=${AI_HOST:http://localhost:8585/}
diff --git a/src/main/resources/db/migration/V1_10_0__new_table_ai_messages.sql b/src/main/resources/db/migration/V1_10_0__new_table_ai_messages.sql
new file mode 100644
index 00000000..3dd6058f
--- /dev/null
+++ b/src/main/resources/db/migration/V1_10_0__new_table_ai_messages.sql
@@ -0,0 +1,15 @@
+CREATE TABLE IF NOT EXISTS ai_messages (
+ aim_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ aic_id UUID REFERENCES ai_conversations(aic_id) ON DELETE CASCADE NOT NULL,
+ is_user BOOLEAN NOT NULL,
+ message bytea,
+ insert_date TIMESTAMP NOT NULL DEFAULT now(),
+ update_date TIMESTAMP NOT NULL DEFAULT now()
+);
+
+COMMENT ON TABLE ai_messages IS 'Table to store AI chat messages.';
+COMMENT ON COLUMN ai_messages.aic_id IS 'It indicates the conversation for the current entry. References the primary key in the ai_conversations table.';
+COMMENT ON COLUMN ai_messages.is_user IS 'Indicate the author of the message: if true, the message is from the user; otherwise, it is from the AI.';
+COMMENT ON COLUMN ai_messages.message IS 'Base64 of compressed message.';
+COMMENT ON COLUMN ai_messages.insert_date IS 'Creation date of this row.';
+COMMENT ON COLUMN ai_messages.update_date IS 'Last update date of this row.';
diff --git a/src/main/resources/db/migration/V1_9_0__new_table_ai_conversations.sql b/src/main/resources/db/migration/V1_9_0__new_table_ai_conversations.sql
new file mode 100644
index 00000000..69637acc
--- /dev/null
+++ b/src/main/resources/db/migration/V1_9_0__new_table_ai_conversations.sql
@@ -0,0 +1,23 @@
+CREATE TABLE IF NOT EXISTS ai_conversations (
+ aic_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ usr_id UUID REFERENCES users(usr_id) ON DELETE CASCADE NOT NULL,
+ key TEXT NOT NULL,
+ checksum TEXT,
+ context TEXT,
+ size BIGINT,
+ insert_date TIMESTAMP NOT NULL DEFAULT now(),
+ update_date TIMESTAMP NOT NULL DEFAULT now(),
+ CONSTRAINT uc_aic_usr_id_key UNIQUE (usr_id, key)
+);
+
+COMMENT ON TABLE ai_conversations IS 'Table to store AI chat conversation.';
+COMMENT ON COLUMN ai_conversations.aic_id IS 'Primary key, serial.';
+COMMENT ON COLUMN ai_conversations.usr_id IS 'It indicates the user for the current entry. References the primary key in the users table.';
+COMMENT ON COLUMN ai_conversations.key IS 'Concatenation of project name, diagram path and plugin name.';
+COMMENT ON COLUMN ai_conversations.checksum IS 'Checksum of last diagram files sent.';
+COMMENT ON COLUMN ai_conversations.context IS 'AI context of conversation.';
+COMMENT ON COLUMN ai_conversations.size IS 'Size of all conversation messages.';
+COMMENT ON COLUMN ai_conversations.insert_date IS 'Creation date of this row.';
+COMMENT ON COLUMN ai_conversations.update_date IS 'Last update date of this row.';
+
+COMMENT ON CONSTRAINT uc_aic_usr_id_key ON ai_conversations IS 'Last update date of this row.';
diff --git a/src/test/java/com/ditrit/letomodelizerapi/controller/AIControllerTest.java b/src/test/java/com/ditrit/letomodelizerapi/controller/AIControllerTest.java
index a02741fe..43d877af 100644
--- a/src/test/java/com/ditrit/letomodelizerapi/controller/AIControllerTest.java
+++ b/src/test/java/com/ditrit/letomodelizerapi/controller/AIControllerTest.java
@@ -1,10 +1,16 @@
package com.ditrit.letomodelizerapi.controller;
+import com.ditrit.letomodelizerapi.controller.model.QueryFilter;
import com.ditrit.letomodelizerapi.helper.MockHelper;
-import com.ditrit.letomodelizerapi.model.ai.AIRequestRecord;
+import com.ditrit.letomodelizerapi.model.ai.AIConversationRecord;
+import com.ditrit.letomodelizerapi.model.ai.AICreateFileRecord;
+import com.ditrit.letomodelizerapi.persistence.model.AIConversation;
+import com.ditrit.letomodelizerapi.persistence.model.AIMessage;
import com.ditrit.letomodelizerapi.persistence.model.User;
import com.ditrit.letomodelizerapi.service.AIService;
+import com.ditrit.letomodelizerapi.service.UserPermissionService;
import com.ditrit.letomodelizerapi.service.UserService;
+import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.ws.rs.core.Response;
@@ -16,8 +22,13 @@
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -29,17 +40,19 @@ class AIControllerTest extends MockHelper {
@Mock
UserService userService;
@Mock
- AIService service;
+ UserPermissionService userPermissionService;
+ @Mock
+ AIService aiService;
@InjectMocks
AIController controller;
@Test
- @DisplayName("Test requestAI: should return valid response.")
- void testRequestAI() throws InterruptedException {
+ @DisplayName("Test generateFiles: should return valid response.")
+ void testGenerateFiles() {
User user = new User();
user.setLogin("login");
- AIRequestRecord aiRequestRecord = new AIRequestRecord("@ditrit/kubernator-plugin", "diagram", "I want a sample of kubernetes code");
+ AICreateFileRecord aiCreateFileRecord = new AICreateFileRecord("@ditrit/kubernator-plugin", "diagram", "I want a sample of kubernetes code");
Mockito
.when(userService.getFromSession(Mockito.any()))
@@ -50,12 +63,184 @@ void testRequestAI() throws InterruptedException {
.when(request.getSession())
.thenReturn(session);
- Mockito.when(this.service.sendRequest(aiRequestRecord)).thenReturn("OK");
- final Response response = this.controller.requestAI(request, aiRequestRecord);
+ Mockito.when(aiService.createFile(aiCreateFileRecord)).thenReturn("OK");
+ final Response response = this.controller.generateFiles(request, aiCreateFileRecord);
assertNotNull(response);
assertEquals(HttpStatus.CREATED.value(), response.getStatus());
assertNotNull(response.getEntity());
}
+ @Test
+ @DisplayName("Test createConversations: should return valid response.")
+ void testCreateConversations() throws JsonProcessingException {
+ User user = new User();
+ user.setLogin("login");
+ AIConversationRecord aiConversationRecord = new AIConversationRecord(
+ "project",
+ "diagram",
+ "@ditrit/kubernator-plugin",
+ "checksum",
+ List.of());
+ HttpSession session = Mockito.mock(HttpSession.class);
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+
+ Mockito.when(request.getSession()).thenReturn(session);
+ Mockito.when(userService.getFromSession(Mockito.any())).thenReturn(user);
+ Mockito.when(aiService.createConversation(user, aiConversationRecord)).thenReturn(new AIConversation());
+
+ Response response = this.controller.createConversations(request, aiConversationRecord);
+
+ assertNotNull(response);
+ assertEquals(HttpStatus.CREATED.value(), response.getStatus());
+ assertNotNull(response.getEntity());
+ }
+
+ @Test
+ @DisplayName("Test findAllConversations: should return valid response.")
+ void testFindAllConversations() {
+ User user = new User();
+ user.setLogin("login");
+ HttpSession session = Mockito.mock(HttpSession.class);
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+
+ Mockito.when(request.getSession()).thenReturn(session);
+ Mockito.when(userService.getFromSession(Mockito.any())).thenReturn(user);
+ Mockito.doNothing().when(userPermissionService).checkIsAdmin(Mockito.any(), Mockito.any());
+ Mockito.when(aiService.findAllConversations(Mockito.any(), Mockito.any())).thenReturn(Page.empty());
+
+ Response response = this.controller.findAllConversations(request, mockUriInfo(), new QueryFilter());
+
+ assertNotNull(response);
+ assertEquals(HttpStatus.OK.value(), response.getStatus());
+ assertNotNull(response.getEntity());
+ }
+
+ @Test
+ @DisplayName("Test getConversationById: should return valid response.")
+ void testGetConversationById() {
+ User user = new User();
+ user.setLogin("login");
+ HttpSession session = Mockito.mock(HttpSession.class);
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+
+ Mockito.when(request.getSession()).thenReturn(session);
+ Mockito.when(userService.getFromSession(Mockito.any())).thenReturn(user);
+ Mockito.when(aiService.getConversationById(Mockito.any(), Mockito.any())).thenReturn(new AIConversation());
+
+ Response response = this.controller.getConversationById(request, UUID.randomUUID());
+
+ assertNotNull(response);
+ assertEquals(HttpStatus.OK.value(), response.getStatus());
+ assertNotNull(response.getEntity());
+ }
+
+ @Test
+ @DisplayName("Test updateConversationById: should return valid response.")
+ void testUpdateConversationById() throws JsonProcessingException {
+ User user = new User();
+ user.setLogin("login");
+ AIConversationRecord aiConversationRecord = new AIConversationRecord(
+ "project",
+ "diagram",
+ "plugin",
+ "checksum",
+ List.of()
+ );
+ HttpSession session = Mockito.mock(HttpSession.class);
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+
+ Mockito.when(request.getSession()).thenReturn(session);
+ Mockito.when(userService.getFromSession(Mockito.any())).thenReturn(user);
+ Mockito.when(aiService.updateConversationById(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(new AIConversation());
+
+ Response response = this.controller.updateConversationById(request, UUID.randomUUID(), aiConversationRecord);
+
+ assertNotNull(response);
+ assertEquals(HttpStatus.OK.value(), response.getStatus());
+ assertNotNull(response.getEntity());
+ }
+
+ @Test
+ @DisplayName("Test deleteConversationById: should return valid response for simple user.")
+ void testDeleteConversationById() {
+ User user = new User();
+ user.setLogin("login");
+ HttpSession session = Mockito.mock(HttpSession.class);
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+
+ Mockito.when(request.getSession()).thenReturn(session);
+ Mockito.when(userService.getFromSession(Mockito.any())).thenReturn(user);
+ Mockito.when(userPermissionService.hasPermission(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(false);
+ Mockito.doNothing().when(aiService).deleteConversationById(Mockito.any(), Mockito.any());
+
+ Response response = this.controller.deleteConversationById(request, UUID.randomUUID());
+
+ assertNotNull(response);
+ assertEquals(HttpStatus.NO_CONTENT.value(), response.getStatus());
+ Mockito.verify(aiService, Mockito.times(0)).deleteConversationById(Mockito.any());
+ Mockito.verify(aiService, Mockito.times(1)).deleteConversationById(Mockito.any(), Mockito.any());
+ }
+
+ @Test
+ @DisplayName("Test deleteConversationById: should return valid response for admin.")
+ void testDeleteConversationByIdAsAdmin() {
+ User user = new User();
+ user.setLogin("login");
+ HttpSession session = Mockito.mock(HttpSession.class);
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+
+ Mockito.when(request.getSession()).thenReturn(session);
+ Mockito.when(userService.getFromSession(Mockito.any())).thenReturn(user);
+ Mockito.when(userPermissionService.hasPermission(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(true);
+ Mockito.doNothing().when(aiService).deleteConversationById(Mockito.any());
+
+ Response response = this.controller.deleteConversationById(request, UUID.randomUUID());
+
+ assertNotNull(response);
+ assertEquals(HttpStatus.NO_CONTENT.value(), response.getStatus());
+ Mockito.verify(aiService, Mockito.times(1)).deleteConversationById(Mockito.any());
+ Mockito.verify(aiService, Mockito.times(0)).deleteConversationById(Mockito.any(), Mockito.any());
+ }
+
+ @Test
+ @DisplayName("Test createConversationMessage: should return valid response.")
+ void testCreateConversationMessage() throws IOException {
+ User user = new User();
+ user.setLogin("login");
+ HttpSession session = Mockito.mock(HttpSession.class);
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+ AIMessage message = new AIMessage();
+ message.setMessage("test".getBytes());
+
+ Mockito.when(request.getSession()).thenReturn(session);
+ Mockito.when(userService.getFromSession(Mockito.any())).thenReturn(user);
+ Mockito.when(aiService.sendMessage(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(message);
+
+ Response response = this.controller.createConversationMessage(request, UUID.randomUUID(), "ok");
+
+ assertNotNull(response);
+ assertEquals(HttpStatus.CREATED.value(), response.getStatus());
+ assertNotNull(response.getEntity());
+ }
+
+ @Test
+ @DisplayName("Test findAllMessages: should return valid response.")
+ void testFindAllMessages() {
+ User user = new User();
+ user.setLogin("login");
+ HttpSession session = Mockito.mock(HttpSession.class);
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+
+ Mockito.when(request.getSession()).thenReturn(session);
+ Mockito.when(userService.getFromSession(Mockito.any())).thenReturn(user);
+ Mockito.when(aiService.findAllMessages(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
+ .thenReturn(Page.empty());
+
+ Response response = this.controller.findAllMessages(request, UUID.randomUUID(), mockUriInfo(), new QueryFilter());
+
+ assertNotNull(response);
+ assertEquals(HttpStatus.OK.value(), response.getStatus());
+ assertNotNull(response.getEntity());
+ }
}
diff --git a/src/test/java/com/ditrit/letomodelizerapi/controller/CurrentUserControllerTest.java b/src/test/java/com/ditrit/letomodelizerapi/controller/CurrentUserControllerTest.java
index 8b5f878c..20c0722d 100644
--- a/src/test/java/com/ditrit/letomodelizerapi/controller/CurrentUserControllerTest.java
+++ b/src/test/java/com/ditrit/letomodelizerapi/controller/CurrentUserControllerTest.java
@@ -8,6 +8,7 @@
import com.ditrit.letomodelizerapi.model.user.UserDTO;
import com.ditrit.letomodelizerapi.persistence.model.Permission;
import com.ditrit.letomodelizerapi.persistence.model.User;
+import com.ditrit.letomodelizerapi.service.AIService;
import com.ditrit.letomodelizerapi.service.AccessControlService;
import com.ditrit.letomodelizerapi.service.UserPermissionService;
import com.ditrit.letomodelizerapi.service.UserService;
@@ -39,7 +40,6 @@
@ExtendWith(MockitoExtension.class)
@DisplayName("Test class: CurrentUserController")
class CurrentUserControllerTest extends MockHelper {
-
@Mock
UserService userService;
@@ -49,8 +49,11 @@ class CurrentUserControllerTest extends MockHelper {
@Mock
AccessControlService accessControlService;
+ @Mock
+ AIService aiService;
+
@InjectMocks
- CurrentUserController controller;
+ CurrentUserController currentUserController;
@Test
@DisplayName("Test getMyInformation: should return valid response.")
@@ -77,7 +80,7 @@ void testGetMyInformation() {
.when(userService.getFromSession(Mockito.any()))
.thenReturn(user);
- Response response = controller.getMyInformation(request);
+ Response response = currentUserController.getMyInformation(request);
assertNotNull(response);
assertEquals(HttpStatus.OK.value(), response.getStatus());
@@ -87,7 +90,13 @@ void testGetMyInformation() {
@Test
@DisplayName("Test getPicture: should return valid response.")
void testGetPicture() {
- CurrentUserController controller = new CurrentUserController(userService, userPermissionService, accessControlService, "1");
+ CurrentUserController controller = new CurrentUserController(
+ userService,
+ userPermissionService,
+ accessControlService,
+ aiService,
+ "1"
+ );
User user = new User();
user.setId(UUID.randomUUID());
user.setEmail("email");
@@ -152,7 +161,7 @@ void testGetMyPermissions() {
expectedPermission.setLibraryId(permission.getLibraryId());
List expectedPermissions = List.of(expectedPermission);
- Response response = controller.getMyPermissions(request);
+ Response response = currentUserController.getMyPermissions(request);
assertNotNull(response);
assertEquals(HttpStatus.OK.value(), response.getStatus());
@@ -178,7 +187,7 @@ void testGetMyRoles() {
.when(accessControlService.findAll(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
.thenReturn(Page.empty());
- Response response = controller.getMyRoles(request, mockUriInfo(), new QueryFilter());
+ Response response = currentUserController.getMyRoles(request, mockUriInfo(), new QueryFilter());
assertNotNull(response);
assertEquals(HttpStatus.OK.value(), response.getStatus());
@@ -204,7 +213,7 @@ void testGetMyGroups() {
.when(accessControlService.findAll(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
.thenReturn(Page.empty());
- Response response = controller.getMyGroups(request, mockUriInfo(), new QueryFilter());
+ Response response = currentUserController.getMyGroups(request, mockUriInfo(), new QueryFilter());
assertNotNull(response);
assertEquals(HttpStatus.OK.value(), response.getStatus());
@@ -230,7 +239,33 @@ void testGetMyScopes() {
.when(accessControlService.findAll(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
.thenReturn(Page.empty());
- Response response = controller.getMyScopes(request, mockUriInfo(), new QueryFilter());
+ Response response = currentUserController.getMyScopes(request, mockUriInfo(), new QueryFilter());
+
+ assertNotNull(response);
+ assertEquals(HttpStatus.OK.value(), response.getStatus());
+ assertNotNull(response.getEntity());
+ }
+
+ @Test
+ @DisplayName("Test getMyAIConversations: should return valid response.")
+ void testMyAIConversations() {
+ User user = new User();
+ user.setId(UUID.randomUUID());
+
+ HttpSession session = Mockito.mock(HttpSession.class);
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+
+ Mockito
+ .when(request.getSession())
+ .thenReturn(session);
+ Mockito
+ .when(userService.getFromSession(Mockito.any()))
+ .thenReturn(user);
+ Mockito
+ .when(aiService.findAll(Mockito.any(), Mockito.any(), Mockito.any()))
+ .thenReturn(Page.empty());
+
+ Response response = currentUserController.getMyAIConversations(request, mockUriInfo(), new QueryFilter());
assertNotNull(response);
assertEquals(HttpStatus.OK.value(), response.getStatus());
diff --git a/src/test/java/com/ditrit/letomodelizerapi/controller/UserControllerTest.java b/src/test/java/com/ditrit/letomodelizerapi/controller/UserControllerTest.java
index 9937059d..aab62cb3 100644
--- a/src/test/java/com/ditrit/letomodelizerapi/controller/UserControllerTest.java
+++ b/src/test/java/com/ditrit/letomodelizerapi/controller/UserControllerTest.java
@@ -3,6 +3,7 @@
import com.ditrit.letomodelizerapi.controller.model.QueryFilter;
import com.ditrit.letomodelizerapi.helper.MockHelper;
import com.ditrit.letomodelizerapi.persistence.model.User;
+import com.ditrit.letomodelizerapi.service.AIService;
import com.ditrit.letomodelizerapi.service.AccessControlService;
import com.ditrit.letomodelizerapi.service.UserPermissionService;
import com.ditrit.letomodelizerapi.service.UserService;
@@ -46,6 +47,9 @@ class UserControllerTest extends MockHelper {
@Mock
AccessControlService accessControlService;
+ @Mock
+ AIService aiService;
+
@InjectMocks
UserController controller;
@@ -241,7 +245,7 @@ void testGetScopesOfUser() {
@Test
@DisplayName("Test getPicture: should return valid response.")
void testGetPicture() {
- UserController controller = new UserController(userService, userPermissionService, accessControlService, "1");
+ UserController userController = new UserController(userService, userPermissionService, accessControlService, aiService, "1");
User user = new User();
user.setId(UUID.randomUUID());
user.setEmail("email");
@@ -265,9 +269,61 @@ void testGetPicture() {
.when(userService.getPicture(Mockito.any()))
.thenReturn(picture);
- Response response = controller.getPictureOfUser(request, "test");
+ Response response = userController.getPictureOfUser(request, "test");
+
+ assertNotNull(response);
+ assertEquals(HttpStatus.OK.value(), response.getStatus());
+ }
+
+ @Test
+ @DisplayName("Test getAIConversationsOfUser: should return valid response for our user.")
+ void testGetAIConversationsOfUser() {
+ UUID id = UUID.randomUUID();
+ User user = new User();
+ user.setLogin("login");
+ user.setId(id);
+
+ Mockito.when(userService.getFromSession(Mockito.any())).thenReturn(user);
+ Mockito.when(userService.findByLogin(Mockito.any())).thenReturn(user);
+
+ HttpSession session = Mockito.mock(HttpSession.class);
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+ Mockito.when(request.getSession()).thenReturn(session);
+ Mockito.when(this.userService.findByLogin(Mockito.any())).thenReturn(new User());
+ Mockito.when(this.aiService.findAll(Mockito.any(), Mockito.any(), Mockito.any()))
+ .thenReturn(Page.empty());
+ final Response response = this.controller.getAIConversationsOfUser(request, "login", mockUriInfo(), new QueryFilter());
+
+ assertNotNull(response);
+ assertEquals(HttpStatus.OK.value(), response.getStatus());
+ assertNotNull(response.getEntity());
+ }
+
+ @Test
+ @DisplayName("Test getAIConversationsOfUser: should return valid response for another user.")
+ void testGetAIConversationsOfUserAnother() {
+ User me = new User();
+ me.setLogin("me");
+ me.setId(UUID.randomUUID());
+
+ User user = new User();
+ user.setLogin("login");
+ user.setId(UUID.randomUUID());
+
+ Mockito.when(userService.getFromSession(Mockito.any())).thenReturn(me);
+ Mockito.when(userService.findByLogin(Mockito.any())).thenReturn(user);
+
+ HttpSession session = Mockito.mock(HttpSession.class);
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+ Mockito.when(request.getSession()).thenReturn(session);
+ Mockito.doNothing().when(userPermissionService).checkIsAdmin(Mockito.any(), Mockito.any());
+ Mockito.when(this.userService.findByLogin(Mockito.any())).thenReturn(new User());
+ Mockito.when(this.aiService.findAll(Mockito.any(), Mockito.any(), Mockito.any()))
+ .thenReturn(Page.empty());
+ final Response response = this.controller.getAIConversationsOfUser(request, "login", mockUriInfo(), new QueryFilter());
assertNotNull(response);
assertEquals(HttpStatus.OK.value(), response.getStatus());
+ assertNotNull(response.getEntity());
}
}
diff --git a/src/test/java/com/ditrit/letomodelizerapi/cucumber/StepDefinitions.java b/src/test/java/com/ditrit/letomodelizerapi/cucumber/StepDefinitions.java
index 0a44cd5f..6441bdfb 100644
--- a/src/test/java/com/ditrit/letomodelizerapi/cucumber/StepDefinitions.java
+++ b/src/test/java/com/ditrit/letomodelizerapi/cucumber/StepDefinitions.java
@@ -75,6 +75,7 @@ public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws Certi
.build();
private final ObjectMapper mapper = new ObjectMapper();
private int statusCode;
+ private String body;
private JsonNode json;
private JsonNode resources;
private JsonNode responseObject;
@@ -163,7 +164,7 @@ public void request(String endpoint, String method) {
@When("I request {string} with method {string} with body")
public void requestWithTable(String endpoint, String method, DataTable table) {
- this.requestFull(endpoint, method, createBody(table), MediaType.TEXT_PLAIN);
+ this.requestFull(endpoint, method, createBody(table), MediaType.APPLICATION_JSON);
}
@When("I request {string} with method {string} with json")
@@ -235,6 +236,8 @@ public void requestFull(String endpoint, String method, Entity> body, String c
} catch (IOException e) {
LOGGER.error("Can't read body", e);
}
+ } else {
+ this.body = response.readEntity(String.class);
}
}
@@ -364,6 +367,11 @@ public void expectResponseIsEqualsTo(String value) {
assertEquals(json.toString(), value);
}
+ @Then("I expect body is {string}")
+ public void expectBodyIsEqualsTo(String value) {
+ assertEquals(this.body, value);
+ }
+
@Then("I expect response resources value is {string}")
public void expectResponseIs(String value) {
assertEquals(resources.toString(), value);
@@ -423,6 +431,11 @@ public void cleanLibrary(String url) throws URISyntaxException, IOException, Int
this.clean("libraries", String.format("url=%s", url));
}
+ @And("I clean the AI conversation {string}")
+ public void cleanAiConversation(String key) throws URISyntaxException, IOException, InterruptedException {
+ this.clean("ai/conversations", String.format("key=%s", key));
+ }
+
public void clean(String entity, String query) throws URISyntaxException, IOException, InterruptedException {
this.request(String.format("/%s?%s", entity, query));
if (statusCode == 200 && json.get("totalElements").asInt() > 0) {
diff --git a/src/test/java/com/ditrit/letomodelizerapi/model/mapper/ai/AIMessageToDTOMapperTest.java b/src/test/java/com/ditrit/letomodelizerapi/model/mapper/ai/AIMessageToDTOMapperTest.java
new file mode 100644
index 00000000..9953f9f2
--- /dev/null
+++ b/src/test/java/com/ditrit/letomodelizerapi/model/mapper/ai/AIMessageToDTOMapperTest.java
@@ -0,0 +1,30 @@
+package com.ditrit.letomodelizerapi.model.mapper.ai;
+
+import com.ditrit.letomodelizerapi.model.ai.AIMessageDTO;
+import com.ditrit.letomodelizerapi.persistence.model.AIMessage;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.util.Base64;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@Tag("unit")
+@DisplayName("Test class: AIMessageToDTOMapper")
+class AIMessageToDTOMapperTest {
+
+ @Test
+ @DisplayName("Test apply: should map to dto.")
+ void testApply() {
+ final AIMessageToDTOMapper mapper = new AIMessageToDTOMapper();
+
+ AIMessage message = new AIMessage();
+ message.setMessage("test".getBytes());
+
+ AIMessageDTO dto = mapper.apply(message);
+ assertNotNull(dto);
+ assertEquals("test", new String(Base64.getDecoder().decode(dto.getMessage())));
+ }
+}
diff --git a/src/test/java/com/ditrit/letomodelizerapi/persistence/specification/filter/DatePredicateFilterTest.java b/src/test/java/com/ditrit/letomodelizerapi/persistence/specification/filter/DatePredicateFilterTest.java
index c5b1ab00..a9b97e64 100644
--- a/src/test/java/com/ditrit/letomodelizerapi/persistence/specification/filter/DatePredicateFilterTest.java
+++ b/src/test/java/com/ditrit/letomodelizerapi/persistence/specification/filter/DatePredicateFilterTest.java
@@ -49,25 +49,25 @@ void extractTest() {
assertNotNull(exception);
assertEquals(ErrorType.WRONG_FILTER_VALUE.getMessage(), exception.getError().getMessage());
- DatePredicateFilter filter = new DatePredicateFilter(null, "2019-01-01 00:00:00");
+ DatePredicateFilter filter = new DatePredicateFilter(null, "2019-01-01 00:00:00.000");
assertTrue(filter.extract());
assertEquals(PredicateOperator.EQUALS, filter.getOperator(0));
- assertEquals("2019-01-01 00:00:00", filter.getValue(0));
+ assertEquals("2019-01-01 00:00:00.000", filter.getValue(0));
- filter = new DatePredicateFilter(null, "gt2019-01-01 00:00:00");
+ filter = new DatePredicateFilter(null, "gt2019-01-01 00:00:00.000");
assertTrue(filter.extract());
assertEquals(PredicateOperator.SUPERIOR, filter.getOperator(0));
- assertEquals("2019-01-01 00:00:00", filter.getValue(0));
+ assertEquals("2019-01-01 00:00:00.000", filter.getValue(0));
- filter = new DatePredicateFilter(null, "lt2019-01-01 00:00:00");
+ filter = new DatePredicateFilter(null, "lt2019-01-01 00:00:00.000");
assertTrue(filter.extract());
assertEquals(PredicateOperator.INFERIOR, filter.getOperator(0));
- assertEquals("2019-01-01 00:00:00", filter.getValue(0));
+ assertEquals("2019-01-01 00:00:00.000", filter.getValue(0));
- filter = new DatePredicateFilter(null, "2018-01-01 00:00:00bt2019-01-01 00:00:00");
+ filter = new DatePredicateFilter(null, "2018-01-01 00:00:00.000bt2019-01-01 00:00:00.000");
assertTrue(filter.extract());
assertEquals(PredicateOperator.BETWEEN, filter.getOperator(0));
- assertEquals("2019-01-01 00:00:00", filter.getValue(0));
+ assertEquals("2019-01-01 00:00:00.000", filter.getValue(0));
filter = new DatePredicateFilter(null, "null");
assertTrue(filter.extract());
@@ -82,7 +82,7 @@ void extractTest() {
exception = null;
try {
- new DatePredicateFilter(null, "bt2019-01-01 00:00:00").extract();
+ new DatePredicateFilter(null, "bt2019-01-01 00:00:00.000").extract();
} catch (ApiException e) {
exception = e;
}
@@ -96,7 +96,7 @@ void extractTest() {
void getSpecificOperatorTest() {
ApiException exception = null;
try {
- new DatePredicateFilter(null, "2019-01-01 00:00:00aa2019-01-01 00:00:00").extract();
+ new DatePredicateFilter(null, "2019-01-01 00:00:00.000aa2019-01-01 00:00:00.000").extract();
} catch (ApiException e) {
exception = e;
}
@@ -105,7 +105,7 @@ void getSpecificOperatorTest() {
assertEquals(ErrorType.WRONG_FILTER_OPERATOR.getMessage(), exception.getError().getMessage());
exception = null;
try {
- new DatePredicateFilter(null, "not_2019-01-01 00:00:00").extract();
+ new DatePredicateFilter(null, "not_2019-01-01 00:00:00.000").extract();
} catch (ApiException e) {
exception = e;
}
@@ -127,7 +127,7 @@ void getDateTest() {
Date date = null;
exception = null;
try {
- date = filter.getDate("2019-12-01 00:00:00");
+ date = filter.getDate("2019-12-01 00:00:00.000");
} catch (final ApiException e) {
exception = e;
}
@@ -144,27 +144,27 @@ void getPredicateTest() {
final CriteriaQuery query = builder.createQuery(Entity.class);
final Root root = query.from(Entity.class);
- DatePredicateFilter filter = new DatePredicateFilter("name", "2019-01-01 00:00:00|not_2018-01-01 00:00:00");
+ DatePredicateFilter filter = new DatePredicateFilter("name", "2019-01-01 00:00:00.000|not_2018-01-01 00:00:00.000");
assertTrue(filter.extract());
Predicate predicate = filter.getPredicate(builder, root, null);
assertNotNull(predicate);
- filter = new DatePredicateFilter("date", "lt2019-01-01 00:00:00");
+ filter = new DatePredicateFilter("date", "lt2019-01-01 00:00:00.000");
assertTrue(filter.extract());
predicate = filter.getPredicate(builder, root, null);
assertNotNull(predicate);
- filter = new DatePredicateFilter("date", "gt2019-01-01 00:00:00");
+ filter = new DatePredicateFilter("date", "gt2019-01-01 00:00:00.000");
assertTrue(filter.extract());
predicate = filter.getPredicate(builder, root, null);
assertNotNull(predicate);
- filter = new DatePredicateFilter("date", "2019-01-01 00:00:00bt2019-01-02 00:00:00");
+ filter = new DatePredicateFilter("date", "2019-01-01 00:00:00.000bt2019-01-02 00:00:00.000");
assertTrue(filter.extract());
predicate = filter.getPredicate(builder, root, null);
assertNotNull(predicate);
assertEquals(1, filter.getValues().length);
- assertEquals("2019-01-02 00:00:00", filter.getValue(0));
+ assertEquals("2019-01-02 00:00:00.000", filter.getValue(0));
filter = new DatePredicateFilter("date", "NULL");
assertTrue(filter.extract());
@@ -176,17 +176,17 @@ void getPredicateTest() {
predicate = filter.getPredicate(builder, root, null);
assertNotNull(predicate);
- filter = new DatePredicateFilter("date", "NOT_2019-01-01 00:00:00");
+ filter = new DatePredicateFilter("date", "NOT_2019-01-01 00:00:00.000");
assertTrue(filter.extract());
predicate = filter.getPredicate(builder, root, null);
assertNotNull(predicate);
- filter = new DatePredicateFilter("date", "not_2019-01-01 00:00:00bt2019-01-02 00:00:00");
+ filter = new DatePredicateFilter("date", "not_2019-01-01 00:00:00.000bt2019-01-02 00:00:00.000");
assertTrue(filter.extract());
predicate = filter.getPredicate(builder, root, null);
assertNotNull(predicate);
assertEquals(1, filter.getValues().length);
- assertEquals("2019-01-02 00:00:00", filter.getValue(0));
+ assertEquals("2019-01-02 00:00:00.000", filter.getValue(0));
}
class Entity {
diff --git a/src/test/java/com/ditrit/letomodelizerapi/service/AIServiceImplTest.java b/src/test/java/com/ditrit/letomodelizerapi/service/AIServiceImplTest.java
index 4c24df4f..81ea02c2 100644
--- a/src/test/java/com/ditrit/letomodelizerapi/service/AIServiceImplTest.java
+++ b/src/test/java/com/ditrit/letomodelizerapi/service/AIServiceImplTest.java
@@ -1,31 +1,51 @@
package com.ditrit.letomodelizerapi.service;
-import com.ditrit.letomodelizerapi.model.ai.AIRequestRecord;
+import com.ditrit.letomodelizerapi.model.ai.AIConversationRecord;
+import com.ditrit.letomodelizerapi.model.ai.AICreateFileRecord;
import com.ditrit.letomodelizerapi.model.error.ApiException;
import com.ditrit.letomodelizerapi.model.error.ErrorType;
+import com.ditrit.letomodelizerapi.persistence.model.AIConversation;
+import com.ditrit.letomodelizerapi.persistence.model.AIMessage;
+import com.ditrit.letomodelizerapi.persistence.model.User;
+import com.ditrit.letomodelizerapi.persistence.repository.AIConversationRepository;
+import com.ditrit.letomodelizerapi.persistence.repository.AIMessageRepository;
+import com.fasterxml.jackson.core.JsonProcessingException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.HttpStatus;
import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.*;
@Tag("unit")
@ExtendWith(MockitoExtension.class)
@DisplayName("AIServiceImpl Unit Tests")
class AIServiceImplTest {
+ @Mock
+ AIConversationRepository aiConversationRepository;
+
+ @Mock
+ AIMessageRepository aiMessageRepository;
+
AIServiceImpl newInstance() {
- return new AIServiceImpl("http://localhost:8585/");
+ return new AIServiceImpl(aiConversationRepository, aiMessageRepository, "http://localhost:8585/");
}
private static void mockHttpCall(MockedStatic clientStatic, int expectedStatus, String expectedBody) throws IOException, InterruptedException {
@@ -45,25 +65,24 @@ private static void mockHttpCall(MockedStatic clientStatic, int expe
}
@Test
- @DisplayName("Test sendRequest: should return valid response when a valid request is done")
- void testSendRequest() throws IOException, InterruptedException {
-
+ @DisplayName("Test createFile: should return valid response when a valid request is done")
+ void testCreateFile() throws IOException, InterruptedException {
String expectedBody = "[{\"name\": \"deployment.yaml\", \"content\": \"apiVersion: apps/v1\\nkind: Deployment\"}]";
MockedStatic clientStatic = Mockito.mockStatic(HttpClient.class);
mockHttpCall(clientStatic, HttpStatus.OK.value(), expectedBody);
AIServiceImpl service = newInstance();
- AIRequestRecord aiRequestRecord = new AIRequestRecord("@ditrit/kubernator-plugin", "diagram", "I want a sample of kubernetes code");
- assertEquals(expectedBody, service.sendRequest(aiRequestRecord));
+ AICreateFileRecord aiCreateFileRecord = new AICreateFileRecord("@ditrit/kubernator-plugin", "diagram", "I want a sample of kubernetes code");
+ assertEquals(expectedBody, service.createFile(aiCreateFileRecord));
Mockito.reset();
clientStatic.close();
}
@Test
- @DisplayName("Test sendRequest: should return a 530 due to an error in the body")
- void testSendRequestWrongBody() throws IOException, InterruptedException {
+ @DisplayName("Test createFile: should return a 530 due to an error in the body")
+ void testCreateFileWrongBody() throws IOException, InterruptedException {
MockedStatic clientStatic = Mockito.mockStatic(HttpClient.class);
mockHttpCall(clientStatic,530, null);
@@ -73,8 +92,8 @@ void testSendRequestWrongBody() throws IOException, InterruptedException {
ApiException exception = null;
try {
- AIRequestRecord aiRequestRecord = new AIRequestRecord("@ditrit/kubernator-plugin", "diagram", "coucou");
- service.sendRequest(aiRequestRecord);
+ AICreateFileRecord aiCreateFileRecord = new AICreateFileRecord("@ditrit/kubernator-plugin", "diagram", "coucou");
+ service.createFile(aiCreateFileRecord);
} catch (ApiException e) {
exception = e;
}
@@ -88,9 +107,8 @@ void testSendRequestWrongBody() throws IOException, InterruptedException {
}
@Test
- @DisplayName("Test sendRequest: should return a 206 (Wrong value) due to an error in the url")
- void testSendRequestWrongUrl() throws IOException, InterruptedException {
-
+ @DisplayName("Test createFile: should return a 206 (Wrong value) due to an error in the url")
+ void testCreateFileWrongUrl() throws IOException, InterruptedException {
MockedStatic clientStatic = Mockito.mockStatic(HttpClient.class);
mockHttpCall(clientStatic,400, null);
@@ -99,8 +117,8 @@ void testSendRequestWrongUrl() throws IOException, InterruptedException {
ApiException exception = null;
try {
- AIRequestRecord aiRequestRecord = new AIRequestRecord("@ditrit/kubernator-plugin", "diagram", "coucou");
- service.sendRequest(aiRequestRecord);
+ AICreateFileRecord aiCreateFileRecord = new AICreateFileRecord("@ditrit/kubernator-plugin", "diagram", "coucou");
+ service.createFile(aiCreateFileRecord);
} catch (ApiException e) {
exception = e;
}
@@ -113,5 +131,333 @@ void testSendRequestWrongUrl() throws IOException, InterruptedException {
clientStatic.close();
}
-}
+ @Test
+ @DisplayName("Test createConversation: should throw ENTITY_ALREADY_EXISTS")
+ void testCreateConversation() throws JsonProcessingException {
+ Mockito.when(aiConversationRepository.existsByUserIdAndKey(Mockito.any(), Mockito.any())).thenReturn(true);
+ AIServiceImpl service = newInstance();
+ ApiException exception = null;
+ User user = new User();
+ user.setId(UUID.randomUUID());
+
+ AIConversationRecord conversationRecord = new AIConversationRecord("test", "test", "test", "checksum", List.of());
+
+ try {
+ service.createConversation(user, conversationRecord);
+ } catch (ApiException e) {
+ exception = e;
+ }
+
+ assertNotNull(exception);
+ assertEquals(ErrorType.ENTITY_ALREADY_EXISTS.getStatus(), exception.getStatus());
+ assertEquals(ErrorType.ENTITY_ALREADY_EXISTS.getMessage(), exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("Test createConversation: should conversation with new context")
+ void testCreateConversationWithNewContext() throws IOException, InterruptedException {
+ MockedStatic clientStatic = Mockito.mockStatic(HttpClient.class);
+ mockHttpCall(clientStatic,200, "{\"context\": \"[\\\"newContext\\\"]\"}");
+
+ AIConversation conversation = new AIConversation();
+ conversation.setContext("newContext");
+
+ Mockito.when(aiConversationRepository.existsByUserIdAndKey(Mockito.any(), Mockito.any())).thenReturn(false);
+ Mockito.when(aiConversationRepository.save(Mockito.any())).thenReturn(conversation);
+ AIServiceImpl service = newInstance();
+ User user = new User();
+ user.setId(UUID.randomUUID());
+ AIConversationRecord conversationRecord = new AIConversationRecord("test", "test", "test", "checksum", List.of());
+
+ AIConversation expectedConversation = service.createConversation(user, conversationRecord);
+
+ assertEquals("newContext", expectedConversation.getContext());
+
+ Mockito.reset();
+ clientStatic.close();
+ }
+
+ @Test
+ @DisplayName("Test getConversationById: should return conversation")
+ void testGetConversationById() {
+ AIConversation expectedConversation = new AIConversation();
+
+ Mockito.when(aiConversationRepository.findByIdAndUserId(Mockito.any(), Mockito.any()))
+ .thenReturn(Optional.of(expectedConversation));
+ AIServiceImpl service = newInstance();
+ User user = new User();
+ user.setId(UUID.randomUUID());
+
+ AIConversation conversation = service.getConversationById(user, UUID.randomUUID());
+
+ assertEquals(conversation, expectedConversation);
+ }
+
+ @Test
+ @DisplayName("Test getConversationById: should throw exception on unknown id")
+ void testGetConversationByIdThrowException() {
+ Mockito.when(aiConversationRepository.findByIdAndUserId(Mockito.any(), Mockito.any()))
+ .thenReturn(Optional.empty());
+ AIServiceImpl service = newInstance();
+ User user = new User();
+ user.setId(UUID.randomUUID());
+
+ ApiException exception = null;
+
+ try {
+ service.getConversationById(user, UUID.randomUUID());
+ } catch (ApiException e) {
+ exception = e;
+ }
+
+ assertNotNull(exception);
+ assertEquals(ErrorType.ENTITY_NOT_FOUND.getStatus(), exception.getStatus());
+ assertEquals(ErrorType.ENTITY_NOT_FOUND.getMessage(), exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("Test updateConversationById: should return updated conversation")
+ void testUpdateConversationById() throws IOException, InterruptedException {
+ MockedStatic clientStatic = Mockito.mockStatic(HttpClient.class);
+ mockHttpCall(clientStatic,200, "{\"context\": \"[\\\"newContext\\\"]\"}");
+ AIConversation expectedConversation = new AIConversation();
+ expectedConversation.setKey("test/test/test");
+ expectedConversation.setSize(0L);
+
+ Mockito.when(aiConversationRepository.findByIdAndUserId(Mockito.any(), Mockito.any()))
+ .thenReturn(Optional.of(expectedConversation));
+ Mockito.when(aiConversationRepository.save(Mockito.any())).thenReturn(expectedConversation);
+ AIServiceImpl service = newInstance();
+ User user = new User();
+ user.setId(UUID.randomUUID());
+ AIConversationRecord conversationRecord = new AIConversationRecord("test", "test", "test", "checksum", List.of());
+
+ AIConversation conversation = service.updateConversationById(user, UUID.randomUUID(), conversationRecord);
+
+ assertEquals(conversation, expectedConversation);
+
+ Mockito.reset();
+ clientStatic.close();
+ }
+
+ @Test
+ @DisplayName("Test updateConversationById: should throw exception on unknown id")
+ void testUpdateConversationByIdThrowException() throws IOException {
+ Mockito.when(aiConversationRepository.findByIdAndUserId(Mockito.any(), Mockito.any()))
+ .thenReturn(Optional.empty());
+ AIServiceImpl service = newInstance();
+ User user = new User();
+ user.setId(UUID.randomUUID());
+ AIConversationRecord conversationRecord = new AIConversationRecord("test", "test", "test", "checksum", List.of());
+
+ ApiException exception = null;
+
+ try {
+ service.updateConversationById(user, UUID.randomUUID(), conversationRecord);
+ } catch (ApiException e) {
+ exception = e;
+ }
+
+ assertNotNull(exception);
+ assertEquals(ErrorType.ENTITY_NOT_FOUND.getStatus(), exception.getStatus());
+ assertEquals(ErrorType.ENTITY_NOT_FOUND.getMessage(), exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("Test deleteConversationById: should delete conversation")
+ void testDeleteConversationById() {
+ AIConversation expectedConversation = new AIConversation();
+
+ Mockito.when(aiConversationRepository.findById(Mockito.any()))
+ .thenReturn(Optional.of(expectedConversation));
+ Mockito.doNothing().when(aiConversationRepository).delete(Mockito.any());
+ AIServiceImpl service = newInstance();
+ ApiException exception = null;
+
+ try {
+ service.deleteConversationById(UUID.randomUUID());
+ } catch (ApiException e) {
+ exception = e;
+ }
+
+ assertNull(exception);
+ }
+
+ @Test
+ @DisplayName("Test deleteConversationById: should throw exception on unknown id")
+ void testDeleteConversationByIdThrowException() {
+ Mockito.when(aiConversationRepository.findById(Mockito.any()))
+ .thenReturn(Optional.empty());
+ AIServiceImpl service = newInstance();
+ ApiException exception = null;
+
+ try {
+ service.deleteConversationById(UUID.randomUUID());
+ } catch (ApiException e) {
+ exception = e;
+ }
+
+ assertNotNull(exception);
+ assertEquals(ErrorType.ENTITY_NOT_FOUND.getStatus(), exception.getStatus());
+ assertEquals(ErrorType.ENTITY_NOT_FOUND.getMessage(), exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("Test deleteConversationById: should delete conversation")
+ void testDeleteConversationByIdAndUser() {
+ AIConversation expectedConversation = new AIConversation();
+
+ Mockito.when(aiConversationRepository.findByIdAndUserId(Mockito.any(), Mockito.any()))
+ .thenReturn(Optional.of(expectedConversation));
+ Mockito.doNothing().when(aiConversationRepository).delete(Mockito.any());
+ AIServiceImpl service = newInstance();
+ User user = new User();
+ user.setId(UUID.randomUUID());
+ ApiException exception = null;
+
+ try {
+ service.deleteConversationById(user, UUID.randomUUID());
+ } catch (ApiException e) {
+ exception = e;
+ }
+
+ assertNull(exception);
+ }
+
+ @Test
+ @DisplayName("Test deleteConversationById: should throw exception on unknown id")
+ void testDeleteConversationByIdAndUserThrowException() {
+ Mockito.when(aiConversationRepository.findByIdAndUserId(Mockito.any(), Mockito.any()))
+ .thenReturn(Optional.empty());
+ AIServiceImpl service = newInstance();
+ User user = new User();
+ user.setId(UUID.randomUUID());
+ ApiException exception = null;
+
+ try {
+ service.deleteConversationById(user, UUID.randomUUID());
+ } catch (ApiException e) {
+ exception = e;
+ }
+
+ assertNotNull(exception);
+ assertEquals(ErrorType.ENTITY_NOT_FOUND.getStatus(), exception.getStatus());
+ assertEquals(ErrorType.ENTITY_NOT_FOUND.getMessage(), exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("Test sendMessage: should AI message")
+ void testSendMessage() throws IOException, InterruptedException {
+ MockedStatic clientStatic = Mockito.mockStatic(HttpClient.class);
+ mockHttpCall(clientStatic,200, "{\"context\": \"[\\\"newContext\\\"]\", \"message\": \"test\"}");
+ AIConversation expectedConversation = new AIConversation();
+ expectedConversation.setId(UUID.randomUUID());
+ expectedConversation.setSize(0L);
+
+ AIMessage expectedMessage = new AIMessage();
+
+ Mockito.when(aiConversationRepository.findByIdAndUserId(Mockito.any(), Mockito.any()))
+ .thenReturn(Optional.of(expectedConversation));
+ Mockito.when(aiConversationRepository.save(Mockito.any())).thenReturn(expectedConversation);
+ Mockito.when(aiMessageRepository.save(Mockito.any())).thenReturn(expectedMessage);
+ AIServiceImpl service = newInstance();
+ User user = new User();
+ user.setId(UUID.randomUUID());
+
+ AIMessage message = service.sendMessage(user, UUID.randomUUID(), "ok");
+
+ assertEquals(message, expectedMessage);
+
+ Mockito.reset();
+ clientStatic.close();
+ }
+
+ @Test
+ @DisplayName("Test sendMessage: should throw exception on unknown id")
+ void testSendMessageThrowException() throws IOException {
+ Mockito.when(aiConversationRepository.findByIdAndUserId(Mockito.any(), Mockito.any()))
+ .thenReturn(Optional.empty());
+ AIServiceImpl service = newInstance();
+ User user = new User();
+ user.setId(UUID.randomUUID());
+ ApiException exception = null;
+
+ try {
+ service.sendMessage(user, UUID.randomUUID(), "test");
+ } catch (ApiException e) {
+ exception = e;
+ }
+
+ assertNotNull(exception);
+ assertEquals(ErrorType.ENTITY_NOT_FOUND.getStatus(), exception.getStatus());
+ assertEquals(ErrorType.ENTITY_NOT_FOUND.getMessage(), exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("Test findAll: should return conversations")
+ void testFindAll() {
+ AIConversation expectedConversation = new AIConversation();
+ expectedConversation.setId(UUID.randomUUID());
+
+ Mockito.when(aiConversationRepository.findAll(Mockito.any(Specification.class), Mockito.any()))
+ .thenReturn(Page.empty());
+ AIServiceImpl service = newInstance();
+ User user = new User();
+ user.setId(UUID.randomUUID());
+
+ assertEquals(Page.empty(), service.findAll(user, Map.of(), Pageable.ofSize(10)));
+ }
+
+ @Test
+ @DisplayName("Test findAllMessages: should return messages")
+ void testFindAllMessages() {
+ AIConversation expectedConversation = new AIConversation();
+ expectedConversation.setId(UUID.randomUUID());
+
+ Mockito.when(aiConversationRepository.findByIdAndUserId(Mockito.any(), Mockito.any()))
+ .thenReturn(Optional.of(expectedConversation));
+ Mockito.when(aiMessageRepository.findAll(Mockito.any(Specification.class), Mockito.any()))
+ .thenReturn(Page.empty());
+ AIServiceImpl service = newInstance();
+ User user = new User();
+ user.setId(UUID.randomUUID());
+
+ assertEquals(Page.empty(), service.findAllMessages(user, UUID.randomUUID(), Map.of(), Pageable.ofSize(10)));
+ }
+
+ @Test
+ @DisplayName("Test findAllMessages: should return messages")
+ void testFindAllMessagesThrowException() {
+ Mockito.when(aiConversationRepository.findByIdAndUserId(Mockito.any(), Mockito.any()))
+ .thenReturn(Optional.empty());
+
+ AIServiceImpl service = newInstance();
+ User user = new User();
+ user.setId(UUID.randomUUID());
+ ApiException exception = null;
+
+ try {
+ service.findAllMessages(user, UUID.randomUUID(), Map.of(), Pageable.ofSize(10));
+ } catch (ApiException e) {
+ exception = e;
+ }
+
+ assertNotNull(exception);
+ assertEquals(ErrorType.ENTITY_NOT_FOUND.getStatus(), exception.getStatus());
+ assertEquals(ErrorType.ENTITY_NOT_FOUND.getMessage(), exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("Test findAllConversations: should return conversations")
+ void testFindAllConversations() {
+ AIConversation expectedConversation = new AIConversation();
+ expectedConversation.setId(UUID.randomUUID());
+
+ Mockito.when(aiConversationRepository.findAll(Mockito.any(Specification.class), Mockito.any()))
+ .thenReturn(Page.empty());
+ AIServiceImpl service = newInstance();
+
+ assertEquals(Page.empty(), service.findAllConversations(Map.of(), Pageable.ofSize(10)));
+ }
+}
diff --git a/src/test/java/com/ditrit/letomodelizerapi/service/UserServiceImplTest.java b/src/test/java/com/ditrit/letomodelizerapi/service/UserServiceImplTest.java
index d15b8c08..3ecd9d6e 100644
--- a/src/test/java/com/ditrit/letomodelizerapi/service/UserServiceImplTest.java
+++ b/src/test/java/com/ditrit/letomodelizerapi/service/UserServiceImplTest.java
@@ -231,6 +231,7 @@ void testGetPicture() throws IOException, InterruptedException {
clientStatic.when(HttpClient::newBuilder).thenReturn(clientBuilder);
assertEquals(expectedResponse, service.getPicture(user));
+ Mockito.reset();
requestStatic.close();
clientStatic.close();
}
diff --git a/src/test/resources/ai/index.php b/src/test/resources/ai/index.php
index 32266ea5..2c8c2a82 100644
--- a/src/test/resources/ai/index.php
+++ b/src/test/resources/ai/index.php
@@ -11,9 +11,19 @@
error_log($type);
error_log(file_get_contents('php://input'));
-if ($type && isset($requestBody['pluginName'])) {
- $fileName = "{$type}_{$requestBody['pluginName']}.json";
- error_log($fileName);
+if ($type == "chat") {
+ $contextValue = isset($requestBody["context"]) ? (int) $requestBody["context"] : 0;
+ $data = [
+ "context" =>1 + $contextValue,
+ "message" => "OK"
+ ];
+ header('Content-Type: application/json');
+ $json = json_encode($data);
+ echo $json;
+ exit;
+} else if ($type && isset($requestBody['pluginName'])) {
+ $fileName = $type . "_" . str_replace("@ditrit/", "", $requestBody['pluginName']) . ".json";
+ error_log($fileName);
if (file_exists($fileName)) {
sleep(rand(5, 10));
diff --git a/src/test/resources/features/AI.feature b/src/test/resources/features/AI.feature
index 00602c70..257b8517 100644
--- a/src/test/resources/features/AI.feature
+++ b/src/test/resources/features/AI.feature
@@ -3,10 +3,61 @@ Feature: ai feature
Scenario: Should return 201 on a valid ai diagram creation
Given I initialize the admin user
- When I request "/ai" with method "POST" with json
+ When I request "/ai/generate" with method "POST" with json
| key | value |
| plugin | terrator-plugin |
| type | diagram |
| description | Test diagram creation |
Then I expect "201" as status code
And I expect response is '[{"name":"main.tf","content":"provider \\"aws\\" {}"}]'
+
+ Scenario: Should return 201 on a valid AI conversation creation
+ Given I initialize the admin user
+ And I clean the AI conversation "TEST/TEST/@ditrit/terrator-plugin"
+
+ When I request "/ai/conversations" with method "POST" with json
+ | key | value | type |
+ | project | TEST | string |
+ | diagram | TEST | string |
+ | plugin | @ditrit/terrator-plugin | string |
+ | checksum | ABCDEF | string |
+ | files | [] | array |
+ Then I expect "201" as status code
+ And I expect response fields length is "5"
+ And I expect response field "key" is "TEST/TEST/@ditrit/terrator-plugin"
+ And I expect response field "id" is "NOT_NULL"
+ And I expect response field "checksum" is "ABCDEF"
+ And I expect response field "size" is "0"
+ And I expect response field "updateDate" is "NOT_NULL"
+ And I set response field "id" to context "conversation_id"
+
+ When I request "/ai/conversations/[conversation_id]" with method "GET"
+ Then I expect "200" as status code
+ And I expect response fields length is "5"
+ And I expect response field "key" is "TEST/TEST/@ditrit/terrator-plugin"
+ And I expect response field "id" is "NOT_NULL"
+ And I expect response field "size" is "0"
+ And I expect response field "checksum" is "ABCDEF"
+ And I expect response field "updateDate" is "NOT_NULL"
+
+ When I request "/users/admin/ai/conversations" with method "GET"
+ Then I expect "200" as status code
+ And I extract resources from response
+ And I expect one resource contains "id" equals to "[conversation_id]"
+
+ When I request "/ai/conversations/[conversation_id]/messages" with method "POST" with body
+ | body | type |
+ | test | String |
+ Then I expect "201" as status code
+ And I expect response fields length is "5"
+ And I expect response field "id" is "NOT_NULL"
+ And I expect response field "aiConversation" is "[conversation_id]"
+ And I expect response field "isUser" is "false"
+ And I expect response field "message" is "H4sIAAAAAAAA//P3BgAt2TbXAgAAAA=="
+ And I expect response field "updateDate" is "NOT_NULL"
+
+ When I request "/ai/conversations/[conversation_id]" with method "DELETE"
+ Then I expect "204" as status code
+
+ When I request "/ai/conversations/[conversation_id]" with method "GET"
+ Then I expect "404" as status code