diff --git a/backend/pom.xml b/backend/pom.xml index fc1b4be5..e0311c9b 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -67,6 +67,34 @@ spring-security-test test + + org.xerial + sqlite-jdbc + 3.42.0.0 + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.hibernate.orm + hibernate-community-dialects + 6.2.0.Final + + + org.mockito + mockito-core + + + org.testng + testng + 6.13.1 + compile + + + org.junit.jupiter + junit-jupiter-api + diff --git a/backend/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java b/backend/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java index 1684fe68..247767d9 100644 --- a/backend/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java +++ b/backend/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java @@ -2,65 +2,79 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.CommandLineRunner; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import java.io.IOException; import java.nio.file.*; import java.util.Arrays; import java.util.List; -@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, SecurityAutoConfiguration.class }) +@SpringBootApplication(exclude = {SecurityAutoConfiguration.class}) +@EnableJpaRepositories("com.UoB.AILearningTool.repository") public class AiLearningToolApplication implements CommandLineRunner { - public static void main(String[] args) { - // disable SSL if launched without keystore.p12 - if (!Files.exists(Paths.get("src/main/resources/keystore.p12"))) { - // Force disable SSL - System.setProperty("server.ssl.enabled", "false"); - } - SpringApplication.run(AiLearningToolApplication.class, args); - } + public static void main(String[] args) { + // disable SSL if launched without keystore.p12 + if (!Files.exists(Paths.get("src/main/resources/keystore.p12"))) { + // Force disable SSL + System.setProperty("server.ssl.enabled", "false"); + } + SpringApplication.run(AiLearningToolApplication.class, args); + } - @Override - public void run(String... args) throws Exception { - Path keystorePath = Paths.get("src", "main", "resources", "keystore.p12"); + @Override + public void run(String... args) throws Exception { + Path keystorePath = Paths.get("src", "main", "resources", "keystore.p12"); - // If there's no keystore in the working directory, skip the SSL setup. - if (!Files.exists(keystorePath)) { - System.out.println("No keystore.p12 found in working directory; skipping SSL copy/overwrite"); - return; - } + // If there's no keystore in the working directory, skip the SSL setup. + if (!Files.exists(keystorePath)) { + System.out.println("No keystore.p12 found in working directory; skipping SSL copy/overwrite"); + return; + } - Path applicationProps = Paths.get("src", "main", "resources", "application.properties"); - overwriteApplicationProperties(applicationProps); - } + // Otherwise, proceed with the copy and overwrite + Path resourcesDir = Paths.get("src", "main", "resources"); + Path targetKeystore = resourcesDir.resolve("keystore.p12"); + copyFile(keystorePath, targetKeystore); + System.out.println("keystore.p12 copied to src/main/resources successfully"); - void copyFile(Path source, Path target) throws IOException { - Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); - } + Path applicationProps = Paths.get("src", "main", "resources", "application.properties"); + overwriteApplicationProperties(applicationProps); + System.out.println("application.properties has been updated successfully"); + } - private void overwriteApplicationProperties(Path propsFile) throws IOException { - List lines = Arrays.asList( - "spring.application.name=AILearningTool", - "server.port=8080", - "spring.servlet.multipart.max-file-size=50MB", - "spring.servlet.multipart.max-request-size=50MB", - "spring.web.resources.static-locations=classpath:/static/", - "", - "spring.http.encoding.charset=UTF-8", - "spring.http.encoding.enabled=true", - "spring.http.encoding.force=true", - "", - "server.ssl.key-store=classpath:keystore.p12", - "server.ssl.key-store-password=ailearntool", - "server.ssl.key-store-type=PKCS12", - "server.ssl.key-alias=myalias" - ); - System.out.println("application.properties has been updated successfully"); - Files.write(propsFile, lines, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE ); - } -} + private void copyFile(Path source, Path target) throws IOException { + Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); + } + + private void overwriteApplicationProperties(Path propsFile) throws IOException { + List lines = Arrays.asList( + "spring.application.name=AILearningTool", + "server.port=8080", + "spring.servlet.multipart.max-file-size=50MB", + "spring.servlet.multipart.max-request-size=50MB", + "spring.web.resources.static-locations=classpath:/static/", + "", + "spring.http.encoding.charset=UTF-8", + "spring.http.encoding.enabled=true", + "spring.http.encoding.force=true", + "", + "server.ssl.key-store=classpath:keystore.p12", + "server.ssl.key-store-password=ailearntool", + "server.ssl.key-store-type=PKCS12", + "server.ssl.key-alias=myalias", + + "spring.datasource.url=jdbc:sqlite:database.db", + "spring.datasource.driver-class-name=org.sqlite.JDBC", + "spring.datasource.username=", + "spring.datasource.password=", + "spring.jpa.database-platform=org.hibernate.dialect.SQLiteDialect", + "spring.jpa.hibernate.ddl-auto=update" + ); + Files.write(propsFile, lines, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/UoB/AILearningTool/Chat.java b/backend/src/main/java/com/UoB/AILearningTool/Chat.java deleted file mode 100644 index 1104e867..00000000 --- a/backend/src/main/java/com/UoB/AILearningTool/Chat.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.UoB.AILearningTool; - -import org.json.*; - -public class Chat { - final private String owner; - private final JSONArray messageHistory = new JSONArray(); - private final String threadID; - - public Chat(User user, String initialMessage, String threadID) { - this.owner = user.getID(); - addUserMessage(user.getID(), initialMessage); - this.threadID = threadID; - } - - public Boolean checkOwner(User user) { - return this.owner.equals(user.getID()); - } - - public String getThreadID() { - return this.threadID; - } - - public void addUserMessage(String userID, String message) { - if (this.owner.equals(userID)) { - JSONObject newMessage = new JSONObject(); - newMessage.put("role", "user"); - newMessage.put("content", message); - this.messageHistory.put(newMessage); - System.out.println("User message: " + message + "has been saved."); - } - } - - public void addAIMessage(String userID, String message) { - if (this.owner.equals(userID)) { - JSONObject newMessage = new JSONObject(); - newMessage.put("role", "assistant"); - newMessage.put("content", message); - this.messageHistory.put(newMessage); - System.out.println("AI message: " + message + "has been saved."); - } - } - - public JSONArray getMessageHistory(User user) { - if (checkOwner(user)) { - return this.messageHistory; - } else {return null;} - } -} diff --git a/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java b/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java index b14374a3..dc86fdd6 100644 --- a/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java +++ b/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java @@ -1,72 +1,102 @@ package com.UoB.AILearningTool; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; +import com.UoB.AILearningTool.model.ChatEntity; +import com.UoB.AILearningTool.model.UserEntity; +import com.UoB.AILearningTool.repository.ChatRepository; +import com.UoB.AILearningTool.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.Optional; -// Communication with SQL database. @Service public class DatabaseController { - private Map users = new HashMap<>(); - private Map chats = new HashMap<>(); + private final UserRepository userRepository; + private final ChatRepository chatRepository; - public User getUser(String userID) { - return users.get(userID); + @Autowired + public DatabaseController(UserRepository userRepository, ChatRepository chatRepository) { + this.userRepository = userRepository; + this.chatRepository = chatRepository; } - public DatabaseController() { - // TODO: Connect to a MariaDB database. + public UserEntity getUser(String username) { + return userRepository.findById(username).orElse(null); } - // Create a new user and return their ID for cookie assignment - public String addUser(boolean optionalConsent) { - User user = new User(optionalConsent); - String id = user.getID(); - // TODO: Add a user profile record to the MariaDB database. - users.put(id, user); - return id; + // Create a new account + public boolean addUser(String username, String password) { + if (userRepository.existsById(username)) { + return false; + } + + UserEntity user = new UserEntity(username, password); + userRepository.save(user); + return true; } - // Remove all data stored about the user (profile, chat, etc.) - public boolean removeUser(String id) { - // TODO: Remove a user profile record from the MariaDB database. - if (users.containsKey(id)) { - users.remove(id); + // Delete existing user account + public boolean removeUser(String sessionID) { + Optional user = userRepository.findBySessionID(sessionID); + + // TODO: Delete chats as well + + if (user.isPresent()) { + userRepository.delete(user.get()); return true; - } else {return false;} + } else { + return false; + } } // Creates a new chat - public String createChat(User user, String initialMessage, String threadID) { + public String createChat(UserEntity user, String initialMessage, String threadID) { String id = StringTools.RandomString(20); - chats.put(id, new Chat(user, initialMessage, threadID)); - return id; + ChatEntity chat = new ChatEntity(user, initialMessage, threadID); + chatRepository.save(chat); + return chat.getChatID(); } + // Deletes an existing chat - public Boolean deleteChat(User user, String chatID) { - Boolean success; - Chat chat = chats.get(chatID); - if (chat != null) { - if (chat.checkOwner(user)) { - chats.remove(chatID); - return true; - } + public boolean deleteChat(UserEntity user, String chatID) { + Optional chatOpt = chatRepository.findById(chatID); + if (chatOpt.isPresent() && chatOpt.get().getOwner().getUsername().equals(user.getUsername())) { + chatRepository.deleteById(chatID); + return true; } return false; } - public Chat getChat(User user, String chatID) { - Chat chat = chats.get(chatID); - if (chat != null) { - if (chat.checkOwner(user)) { - return chat; - } else {return null;} - } else { + public ChatEntity createChat(String sessionID, String initialMessage) { + Optional userOpt = userRepository.findBySessionID(sessionID); + if (userOpt.isEmpty()) { + return null; + } + + UserEntity user = userOpt.get(); + ChatEntity chat = new ChatEntity(user, initialMessage, sessionID); + chatRepository.save(chat); + + return chat; + } + + public ChatEntity getChat(String username, String chatID) { + Optional userOpt = userRepository.findById(username); + if (userOpt.isEmpty()) { return null; } + + Optional chatOpt = chatRepository.findById(chatID); + return chatOpt.filter(chat -> chat.getOwner().getUsername().equals(username)).orElse(null); } -} + public boolean deleteChat(String username, String chatID) { + Optional chatOpt = chatRepository.findById(chatID); + if (chatOpt.isPresent() && chatOpt.get().getOwner().getUsername().equals(username)) { + chatRepository.deleteById(chatID); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java b/backend/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java index 231c2e8a..e0196e59 100644 --- a/backend/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java +++ b/backend/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java @@ -1,5 +1,6 @@ package com.UoB.AILearningTool; +import com.UoB.AILearningTool.model.ChatEntity; import org.json.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,7 +84,7 @@ public int runThread(String threadID) { } // Check if a run is in progress for a thread - public boolean isLocked(Chat chat) { + public boolean isLocked(ChatEntity chat) { HttpRequest listMessagesRequest = HttpRequest .newBuilder(URI.create("https://api.openai.com/v1/threads/" + chat.getThreadID() + "/runs")) .headers("Content-Type", "application/json", @@ -138,7 +139,7 @@ public WatsonxResponse getLastThreadMessage(String threadID) { } // Add a message to an OpenAI thread. - public Integer sendUserMessage(Chat chat, String message) { + public Integer sendUserMessage(ChatEntity chat, String message) { // Creating a request body JSONObject newMessage = new JSONObject(); newMessage.put("role", "user"); diff --git a/backend/src/main/java/com/UoB/AILearningTool/SpringController.java b/backend/src/main/java/com/UoB/AILearningTool/SpringController.java index d1b40ced..4baca76b 100644 --- a/backend/src/main/java/com/UoB/AILearningTool/SpringController.java +++ b/backend/src/main/java/com/UoB/AILearningTool/SpringController.java @@ -1,267 +1,251 @@ package com.UoB.AILearningTool; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletResponse; -import org.json.JSONArray; -import org.json.JSONObject; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.*; +import com.UoB.AILearningTool.model.ChatEntity; +import com.UoB.AILearningTool.model.UserEntity; +import com.UoB.AILearningTool.repository.ChatRepository; +import com.UoB.AILearningTool.repository.UserRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import java.io.IOException; - import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.*; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.*; -@RestController // Use RestController for handling HTTP requests -@Service @Controller +@RestController public class SpringController { - private final Logger log = LoggerFactory.getLogger(SpringController.class); + private static final Logger log = LoggerFactory.getLogger(SpringController.class); private final DatabaseController DBC; - private final OpenAIAPIController WXC; + private final OpenAIAPIController OAIC; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ChatRepository chatRepository; @Autowired - public SpringController(DatabaseController DBC, OpenAIAPIController WXC) { + public SpringController(DatabaseController DBC, OpenAIAPIController OAIC) { this.DBC = DBC; - this.WXC = WXC; + this.OAIC = OAIC; } - private final Map userStore = new ConcurrentHashMap<>(); - @PostMapping("/login") - public ResponseEntity> loginUser(@RequestBody Map credentials) { + // User registration + @PostMapping("/register") + public ResponseEntity> registerUser(@RequestBody Map credentials) { String username = credentials.get("username"); String password = credentials.get("password"); - if (userStore.containsKey(username) && userStore.get(username).equals(password)) { - return ResponseEntity.ok(Collections.singletonMap("success", true)); + + if (username == null || password == null) { + return ResponseEntity.status(400).build(); + } + + boolean success = DBC.addUser(username, password); + + if (success) { + return ResponseEntity.ok().build(); } else { - return ResponseEntity.badRequest().body(Collections.singletonMap("message", "Invalid username or password")); + return ResponseEntity.status(403).build(); } } - @PostMapping("/register") - public ResponseEntity> registerUser(@RequestBody Map credentials) { + // User Login + @PostMapping("/login") + public ResponseEntity> loginUser(@RequestBody Map credentials) { String username = credentials.get("username"); String password = credentials.get("password"); - if (userStore.containsKey(username)) { - return ResponseEntity.badRequest().body(Collections.singletonMap("message", "Username already exists")); + + Optional userOptional = userRepository.findByUsername(username); + + if (userOptional.isPresent() && userOptional.get().getPassword().equals(password)) { + UserEntity user = userOptional.get(); + String sessionID = user.getSessionID(); + + List chats = chatRepository.findByOwner(user); + List chatIDs = new ArrayList<>(); + + for (ChatEntity chat : chats) { + chatIDs.add(chat.getChatID()); + // TODO: Chat titles + // chat.getMessageHistory(user).getJSONObject(0).getJSONObject(0).getString("content"); + } + + return ResponseEntity.ok(Map.of( + "sessionID", sessionID, + "chatIDs", chatIDs + )); } else { - userStore.put(username, password); - return ResponseEntity.ok(Collections.singletonMap("success", true)); + return ResponseEntity.status(401).build(); } } - // Assign a unique user ID for the user. - @GetMapping("/signup") - public void signup(@CookieValue(value = "optionalConsent", defaultValue = "false") boolean optionalConsent, - HttpServletResponse response) { - Cookie userIDCookie = new Cookie("userID", DBC.addUser(optionalConsent)); - userIDCookie.setMaxAge(30 * 24 * 60 * 60); // Cookie will expire in 30 days - userIDCookie.setSecure(false); -// userIDCookie.setAttribute("SameSite", "None"); - userIDCookie.setPath("/"); - response.addCookie(userIDCookie); - log.info("Assigned a new userID."); + + // Check if the user exists + @GetMapping("/checkSession") + public ResponseEntity> checkSession(@RequestParam String sessionID) { + boolean isLoggedIn = userRepository.findBySessionID(sessionID).isPresent(); + if (isLoggedIn) { + return ResponseEntity.ok().build(); + } else { + return ResponseEntity.status(401).build(); + } } - // If user revokes their consent for data storage / optional cookies, - // remove all data stored about them. + // Delete user @GetMapping("/revokeConsent") - public void revokeConsent(@CookieValue(value = "userID", defaultValue = "") String userID, - HttpServletResponse response) { - if (DBC.removeUser(userID)) { - response.setStatus(200); - Cookie cookie = new Cookie("userID", ""); - cookie.setMaxAge(0); - response.addCookie(cookie); - log.info("Revoked consent for user, ID: {}", userID); + public ResponseEntity> revokeConsent(@RequestParam String sessionID) { + boolean success = DBC.removeUser(sessionID); + + if (success) { + return ResponseEntity.ok().build(); } else { - response.setStatus(400); + return ResponseEntity.status(404).build(); } - } + // Create a new chat session with Watsonx - @GetMapping("/createChat") - public void createChat(@CookieValue(value = "userID", defaultValue = "") String userID, - @RequestParam(name = "initialMessage") String initialMessage, - HttpServletResponse response) throws IOException { - response.setContentType("text/plain; charset=utf-8"); // Set the content type to text - - // Get the user - User user = DBC.getUser(userID); - if (user == null) { - response.setStatus(401); - response.getWriter().write("User doesn't exist!"); - log.warn("User {} doesn't exist!", userID); - return; + @PostMapping("/createChat") + public ResponseEntity> createChat(@RequestBody Map request) { + String sessionID = request.get("sessionID"); + String initialMessage = request.get("initialMessage"); + + // Check if user is authorised + Optional userOptional = userRepository.findBySessionID(sessionID); + if (userOptional.isEmpty()) { + return ResponseEntity.status(401) + .body(Collections.singletonMap("message", "Invalid session")); } + + UserEntity user = userOptional.get(); + + ChatEntity chat = new ChatEntity(user, initialMessage, this.OAIC.createThread()); + chatRepository.save(chat); + String chatID = chat.getChatID(); - // Create new chat in the database - String chatID = DBC.createChat(user, initialMessage, WXC.createThread()); - Chat chat = DBC.getChat(user, chatID); // Add initial message to a new thread - Integer sendMessageResultCode = WXC.sendUserMessage(chat, initialMessage); + Integer sendMessageResultCode = OAIC.sendUserMessage(chat, initialMessage); if (sendMessageResultCode != 200) { - try { - response.getWriter().write("Unable to add initial message to the thread."); - response.setStatus(500); - return; - } catch (IOException e) { - log.error(String.valueOf(e)); - return; - } + return ResponseEntity.status(500).body(Collections.singletonMap("message", "Unable to add initial message to the thread.")); } // Run the new thread - int runThreadResultCode = WXC.runThread(chat.getThreadID()); + int runThreadResultCode = OAIC.runThread(chat.getThreadID()); if (runThreadResultCode != 200) { - try { - response.getWriter().write("Unable to run a new thread."); - response.setStatus(500); - return; - } catch (IOException e) { - log.error(String.valueOf(e)); - return; - } + return ResponseEntity.status(500).body(Collections.singletonMap("message", "Unable to run a new thread.")); } // Wait until any runs for a thread are completed - while (WXC.isLocked(chat)) { + while (OAIC.isLocked(chat)) { log.info("Thread is busy"); } // Get the response from the AI and save it to the database - WatsonxResponse AIResponse = WXC.getLastThreadMessage(chat.getThreadID()); + WatsonxResponse AIResponse = OAIC.getLastThreadMessage(chat.getThreadID()); if (AIResponse.statusCode != 200) { - try { - response.getWriter().write("Unable to get last thread message."); - response.setStatus(500); - return; - } catch (IOException e) { - log.error(String.valueOf(e)); - return; - } + return ResponseEntity.status(500).body(Collections.singletonMap("message", "Unable to get last thread message.")); } - chat.addAIMessage(userID, AIResponse.responseText); - response.getWriter().write(chatID); - response.setStatus(200); - } - // Send a message to the chat and receive a response - @GetMapping("/sendMessage") - public void sendMessage(@CookieValue(value = "userID", defaultValue = "") String userID, - @RequestParam(name = "newMessage") String newMessage, - @RequestParam(name = "chatID") String chatID, - HttpServletResponse response) throws IOException { - response.setContentType("text/plain; charset=utf-8"); - response.setStatus(401); // Default - - // Get the user and chat - User user = DBC.getUser(userID); - Chat chat = DBC.getChat(user, chatID); - - // Check if the chat is available - if (chat == null) { - try { - response.getWriter().write("Chat doesn't exist or you are not authorized to access it."); - response.setStatus(404); - return; - } catch (IOException e) { - log.warn(String.valueOf(e)); - return; - } + chat.addAIMessage(user, AIResponse.responseText); + chatRepository.save(chat); + + return ResponseEntity.ok(Map.of( + "chatID", chatID)); + } + + + @PostMapping("/sendMessage") + public ResponseEntity> sendMessage(@RequestBody Map request) { + String sessionID = request.get("sessionID"); + String chatID = request.get("chatID"); + String newMessage = request.get("newMessage"); + + UserEntity user = userRepository.findBySessionID(sessionID).orElse(null); + if (user == null) { + return ResponseEntity.status(401) + .body(Collections.singletonMap("message", "User not found")); + } + + ChatEntity chat = chatRepository.findById(chatID).orElse(null); + if (chat == null || !chat.getOwner().getSessionID().equals(sessionID)) { + return ResponseEntity.status(404) + .body(Collections.singletonMap("message", "Chat not found")); } // Add a message to the thread - int sendMessageResultCode = WXC.sendUserMessage(chat, newMessage); + int sendMessageResultCode = OAIC.sendUserMessage(chat, newMessage); if (sendMessageResultCode != 200) { - try { - response.getWriter().write("Unable to add message to the thread."); - response.setStatus(sendMessageResultCode); - return; - } catch (IOException e) { - log.warn(String.valueOf(e)); - return; - } + return ResponseEntity.status(sendMessageResultCode) + .body(Collections.singletonMap("message", "Unable to add message to the thread.")); } // Run the thread - int runThreadResultCode = WXC.runThread(chat.getThreadID()); + int runThreadResultCode = OAIC.runThread(chat.getThreadID()); if (runThreadResultCode != 200) { - try { - response.getWriter().write("Unable to run the thread."); - response.setStatus(500); - return; - } catch (IOException e) { - log.warn(String.valueOf(e)); - return; - } + return ResponseEntity.status(500) + .body(Collections.singletonMap("message", "Unable to run the thread.")); } // Wait until any runs for a thread are completed - while (WXC.isLocked(chat)) { + while (OAIC.isLocked(chat)) { log.info("Thread is busy"); } // Add new message to the message history - WatsonxResponse AIResponse = WXC.getLastThreadMessage(chat.getThreadID()); + WatsonxResponse AIResponse = OAIC.getLastThreadMessage(chat.getThreadID()); if (AIResponse.statusCode != 200) { - try { - response.getWriter().write("Unable to receive the new AI response."); - response.setStatus(runThreadResultCode); - return; - } catch (IOException e) { - log.warn(String.valueOf(e)); - } + return ResponseEntity.status(404) + .body(Collections.singletonMap("message", "Unable to receive the new AI response.")); } - chat.addUserMessage(userID, newMessage); - chat.addAIMessage(userID, AIResponse.responseText); - JSONObject responseJSON = new JSONObject(); - responseJSON.put("role", "assistant"); - responseJSON.put("content", AIResponse.responseText); - response.getWriter().write(responseJSON.toString()); - response.setStatus(200); + chat.addUserMessage(user, newMessage); + chat.addAIMessage(user, AIResponse.responseText); + chatRepository.save(chat); + return ResponseEntity.ok(Map.of( + "role", "assistant", + "content", AIResponse.responseText + )); } - // Get message history from a chat - @GetMapping("/getChatHistory") - public void getChatHistory(@CookieValue(value = "userID", defaultValue = "") String userID, - @RequestParam(name = "chatID") String chatID, - HttpServletResponse response) { - response.setContentType("text/plain; charset=utf-8"); - User user = DBC.getUser(userID); - Chat chat = DBC.getChat(DBC.getUser(userID), chatID); - if (chat == null) { - response.setStatus(404); - try { - response.getWriter().write("Chat doesn't exist or you are not authorized to access it."); - return; - } catch (IOException e) { - log.warn(String.valueOf(e)); - } + @PostMapping( "/getChatHistory") + public ResponseEntity> getChatHistory(@RequestBody Map request) { + String sessionID = request.get("sessionID"); + String chatID = request.get("chatID"); + + UserEntity user = userRepository.findBySessionID(sessionID).orElse(null); + if (user == null) { + return ResponseEntity.status(401) + .body(Collections.singletonMap("message", "User doesn't exist")); } - JSONArray messageHistory = chat.getMessageHistory(user); + ChatEntity chat = chatRepository.findById(chatID).orElse(null); - // If we reach here, chat != null and messageHistory != null - response.setStatus(200); - try { - response.getWriter().write(messageHistory.toString()); - } catch (IOException e) { - log.warn(String.valueOf(e)); - response.setStatus(500); + if (chat == null || !chat.getOwner().getSessionID().equals(sessionID)) { + return ResponseEntity.status(404) + .body(Collections.singletonMap("message", "Chat doesn't exist or you are not authorized to access it.")); } + + return ResponseEntity.status(200). + body(Map.of("history", chat.getMessageHistory(user).toString())); + } + + @GetMapping("/getUserChats") + public ResponseEntity> getUserChats(@RequestParam String sessionID) { + Optional user = userRepository.findBySessionID(sessionID); + if (user.isEmpty()) { + return ResponseEntity.status(401).body(Collections.singletonMap("message", "User not found")); + } + + List chats = chatRepository.findByOwner(user.get()); + List> chatList = new ArrayList<>(); + + for (ChatEntity chat : chats) { + chatList.add(Map.of("chatID", chat.getChatID(), "title", chat.getMessageHistory(user.get()).getJSONObject(0).getString("content"))); + } + + return ResponseEntity.ok(Collections.singletonMap("chatList", chatList)); } -} + +} \ No newline at end of file diff --git a/backend/src/main/java/com/UoB/AILearningTool/StringTools.java b/backend/src/main/java/com/UoB/AILearningTool/StringTools.java index 3441a011..2508b513 100644 --- a/backend/src/main/java/com/UoB/AILearningTool/StringTools.java +++ b/backend/src/main/java/com/UoB/AILearningTool/StringTools.java @@ -7,6 +7,10 @@ // StringTools create / transform strings to the required formats. public class StringTools { + public static String generateSessionID() { + return RandomString(15); + } + // Generates a random string of specific size (e.g. for userID / chatID) public static String RandomString(int n) { String newString = ""; diff --git a/backend/src/main/java/com/UoB/AILearningTool/User.java b/backend/src/main/java/com/UoB/AILearningTool/User.java deleted file mode 100644 index bda2cbbf..00000000 --- a/backend/src/main/java/com/UoB/AILearningTool/User.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.UoB.AILearningTool; - -import java.time.LocalDateTime; -import java.util.ArrayList; - -// Class representing a user profile (someone who consented to optional cookies). -public class User { - private final String id; - private boolean optionalConsent = false; - private LocalDateTime lastActivityTime; - - public String getID() { - updateLastActivityTime(); - return this.id; - } - - public boolean getOptionalConsent() { - updateLastActivityTime(); - return this.optionalConsent; - } - - public void updateLastActivityTime() { - this.lastActivityTime = LocalDateTime.now(); - } - - - // Create a user - public User(boolean optionalConsent) { - updateLastActivityTime(); - this.id = StringTools.RandomString(25); - this.optionalConsent = optionalConsent; - } -} diff --git a/backend/src/main/java/com/UoB/AILearningTool/model/ChatEntity.java b/backend/src/main/java/com/UoB/AILearningTool/model/ChatEntity.java new file mode 100644 index 00000000..54f8802e --- /dev/null +++ b/backend/src/main/java/com/UoB/AILearningTool/model/ChatEntity.java @@ -0,0 +1,90 @@ +package com.UoB.AILearningTool.model; + +import com.UoB.AILearningTool.StringTools; +import jakarta.persistence.*; +import org.json.JSONArray; +import org.json.JSONObject; + +@Entity +@Table(name = "chats") +public class ChatEntity { + @Id + @Column(name = "chatid", unique = true, nullable = false) + private String chatID; + + @ManyToOne + @JoinColumn(name = "username", nullable = false) + private UserEntity owner; + + @Column(name = "threadid", unique = true, nullable = false) + private String threadID; + + @Column(name = "message_history", columnDefinition = "TEXT") + private String messageHistory; + + + public ChatEntity() {} + + public ChatEntity(UserEntity owner, String initialMessage, String threadID) { + this.chatID = StringTools.RandomString(32); + this.owner = owner; + this.threadID = threadID; + this.messageHistory = new JSONArray().put( + new JSONObject().put( + "role", "user" + ).put("content", initialMessage) + ).toString(); + } + + public String getChatID() { + return chatID; + } + + public String getThreadID() { + return threadID; + } + + public void setChatID(String chatID) { + this.chatID = chatID; + } + + public UserEntity getOwner() { + return owner; + } + + + public JSONArray getMessageHistory(UserEntity user) { + if (this.getOwner().equals(user)) { + return new JSONArray(this.messageHistory); + } else {return null;} + + } + + public void setMessageHistory(JSONArray newHistory) { + this.messageHistory = new JSONArray(newHistory).toString(); + } + + public void addUserMessage(UserEntity user, String message) { + if (this.getOwner().equals(user)) { + this.messageHistory = this.getMessageHistory(user).put( + new JSONObject().put( + "role", "user" + ).put( + "content", message + ) + ).toString(); + } + } + + public void addAIMessage(UserEntity user, String message) { + if (this.getOwner().equals(user)) { + this.messageHistory = this.getMessageHistory(user).put( + new JSONObject().put( + "role", "assistant" + ).put( + "content", message + ) + ).toString(); + } + } +} diff --git a/backend/src/main/java/com/UoB/AILearningTool/model/UserEntity.java b/backend/src/main/java/com/UoB/AILearningTool/model/UserEntity.java new file mode 100644 index 00000000..89a3b8ad --- /dev/null +++ b/backend/src/main/java/com/UoB/AILearningTool/model/UserEntity.java @@ -0,0 +1,55 @@ +package com.UoB.AILearningTool.model; + +import jakarta.persistence.*; +import java.util.List; +import com.UoB.AILearningTool.StringTools; +import java.security.*; + +@Entity +@Table(name = "users") +public class UserEntity { + @Id + @Column(name = "userid", unique = true, nullable = false) + private String userID; + + @Column(name = "username", unique = true, nullable = false) + private String username; + @Column(name = "password", nullable = false) + private String password; + + @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL, orphanRemoval = true) + private List chats; + + public UserEntity() {} + + @Column(name = "sessionid", unique = true, nullable = false) + private String sessionID; + + public UserEntity(String username, String password) { + this.username = username; + this.password = password; + this.userID = StringTools.RandomString(32); + // TODO: Adjust this later + this.sessionID = StringTools.generateSessionID(); + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getSessionID() { + return sessionID; + } + + public String getUserID() { + return userID; + } + + public void setSessionID(String sessionID) { + this.sessionID = sessionID; + } +} diff --git a/backend/src/main/java/com/UoB/AILearningTool/repository/ChatRepository.java b/backend/src/main/java/com/UoB/AILearningTool/repository/ChatRepository.java new file mode 100644 index 00000000..d6084b57 --- /dev/null +++ b/backend/src/main/java/com/UoB/AILearningTool/repository/ChatRepository.java @@ -0,0 +1,18 @@ +package com.UoB.AILearningTool.repository; + +import com.UoB.AILearningTool.model.ChatEntity; +import com.UoB.AILearningTool.model.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ChatRepository extends JpaRepository { + + +// @Query("SELECT c FROM ChatEntity c WHERE c.threadID = :threadID") +// Optional findBySessionID(@Param("threadID") String sessionID); + + List findByOwner(UserEntity owner); +} diff --git a/backend/src/main/java/com/UoB/AILearningTool/repository/UserRepository.java b/backend/src/main/java/com/UoB/AILearningTool/repository/UserRepository.java new file mode 100644 index 00000000..2ce1186b --- /dev/null +++ b/backend/src/main/java/com/UoB/AILearningTool/repository/UserRepository.java @@ -0,0 +1,16 @@ +package com.UoB.AILearningTool.repository; + +import com.UoB.AILearningTool.model.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + @Query("SELECT c FROM UserEntity c WHERE c.sessionID = :sessionID") + Optional findBySessionID(String sessionID); + + Optional findByUsername(String username); +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 74cbe95f..9735a2b1 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -3,6 +3,15 @@ spring.servlet.multipart.max-file-size=50MB spring.servlet.multipart.max-request-size=50MB spring.web.resources.static-locations=classpath:/static/ server.port=8080 + +spring.datasource.url=jdbc:sqlite:database.db +spring.datasource.driver-class-name=org.sqlite.JDBC +spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect +spring.jpa.hibernate.ddl-auto=update +spring.datasource.username= +spring.datasource.password= + server.servlet.encoding.charset=UTF-8 server.servlet.encoding.enabled=true server.servlet.encoding.force=true + diff --git a/backend/src/test/java/com/UoB/AILearningTool/ChatTest.java b/backend/src/test/java/com/UoB/AILearningTool/ChatTest.java index 4a45d9ca..247a3074 100644 --- a/backend/src/test/java/com/UoB/AILearningTool/ChatTest.java +++ b/backend/src/test/java/com/UoB/AILearningTool/ChatTest.java @@ -1,31 +1,40 @@ package com.UoB.AILearningTool; +import com.UoB.AILearningTool.model.ChatEntity; +import com.UoB.AILearningTool.model.UserEntity; import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.util.ArrayList; public class ChatTest { + private OpenAIAPIController OAIC; + private String mockThreadId = "test-thread-id"; + + @BeforeEach + void setUp() { + OAIC = new OpenAIAPIController(); + } + @Test @DisplayName("Check if checkOwner method compares owners correctly.") public void checkOwnerTest() { - OpenAIAPIController OAIC = new OpenAIAPIController(); - User user = new User(true); - Chat chat = new Chat(user, "This is a first message.", OAIC.createThread()); + UserEntity user = new UserEntity("testUserLogin", "testUserPassword"); + ChatEntity chat = new ChatEntity(user, "This is a first message.", mockThreadId); - Assertions.assertTrue(chat.checkOwner(user)); + Assertions.assertEquals(user, chat.getOwner()); } @Test @DisplayName("Check if initial message history is saved correctly.") public void initialMessageHistoryTest() { String initialMessage = "This is a first message."; - OpenAIAPIController OAIC = new OpenAIAPIController(); - User user = new User(true); - Chat chat = new Chat(user, initialMessage, OAIC.createThread()); + UserEntity user = new UserEntity("testUserLogin2", "testUserPassword2"); + ChatEntity chat = new ChatEntity(user, initialMessage, mockThreadId); JSONArray expectedMessageHistory = new JSONArray(); JSONObject onlyMessage = new JSONObject(); @@ -42,11 +51,8 @@ public void initialMessageHistoryTest() { public void addUserMessageTest() { String initialMessage = "This is a first message."; String extraUserMessage = "Tell me a joke."; - - // Create a chat and add 2 user messages. - User user = new User(true); - OpenAIAPIController OAIC = new OpenAIAPIController(); - Chat chat = new Chat(user, initialMessage, OAIC.createThread()); + UserEntity user = new UserEntity("testUserLogin3", "testUserPassword3"); + ChatEntity chat = new ChatEntity(user, initialMessage, mockThreadId); // Create expected message history. JSONArray expectedMessageHistory = new JSONArray(); @@ -59,8 +65,7 @@ public void addUserMessageTest() { onlyMessage.put("content", extraUserMessage); expectedMessageHistory.put(onlyMessage); - // Add a new user message to the chat - chat.addUserMessage(user.getID(), extraUserMessage); + chat.addUserMessage(user, extraUserMessage); JSONArray actualMessageHistory = chat.getMessageHistory(user); Assertions.assertEquals(2, expectedMessageHistory.length()); @@ -73,52 +78,44 @@ public void addUserMessageTest() { @Test @DisplayName("Check if AI messages are added to message history correctly.") public void addAIMessageTest() { - OpenAIAPIController OAIC = new OpenAIAPIController(); String initialMessage = "I need some assistance with finding courses on IBM SkillsBuild platform."; - String extraUserMessage = "Tell me a joke."; - - // Create a chat - User user = new User(true); - Chat chat = new Chat(user, initialMessage, OAIC.createThread()); + String aiResponse = "Here's a helpful response"; + UserEntity user = new UserEntity("testUserLogin4", "testUserPassword4"); + ChatEntity chat = new ChatEntity(user, initialMessage, mockThreadId); - // Add a new user message to the chat - OAIC.runThread(chat.getThreadID()); - chat.addAIMessage(user.getID(), extraUserMessage); + chat.addAIMessage(user, aiResponse); JSONArray actualMessageHistory = chat.getMessageHistory(user); Assertions.assertEquals(2, actualMessageHistory.length()); Assertions.assertEquals("user", actualMessageHistory.getJSONObject(0).getString("role")); Assertions.assertEquals("assistant", actualMessageHistory.getJSONObject(1).getString("role")); Assertions.assertNotNull(actualMessageHistory.getJSONObject(0).getString("content")); - Assertions.assertNotNull(actualMessageHistory.getJSONObject(1).getString("content")); + Assertions.assertEquals(aiResponse, actualMessageHistory.getJSONObject(1).getString("content")); } @Test @DisplayName("Check if message history in a chat can only be accessed by its owner.") public void chatMessageHistoryPermissionTest() { final String initialMessage = "This is a first message."; - User currentUser; - Chat currentChat; - ArrayList users = new ArrayList<>(); - ArrayList chats = new ArrayList<>(); + UserEntity currentUser; + ChatEntity currentChat; + ArrayList users = new ArrayList<>(); + ArrayList chats = new ArrayList<>(); - // Creating chats and checking whether their owners can access their message histories. for (int i = 0; i < 20; i++) { - currentUser = new User(true); + currentUser = new UserEntity("testUserLogin" + i, "testUserPassword" + i); users.add(currentUser); - currentChat = new Chat(currentUser, initialMessage, "TEST"); + currentChat = new ChatEntity(currentUser, initialMessage, mockThreadId); chats.add(currentChat); - // Message history has to be returned to its owner Assertions.assertNotNull(currentChat.getMessageHistory(currentUser)); } for (int i = 0; i < 20; i++) { currentChat = chats.get(i); - // Users that aren't owners of the chat will receive null instead of message history. for (int j = 0; j < 20; j++) { if (i == j) {continue;} currentUser = users.get(j); - JSONArray actualMessageHistory = currentChat.getMessageHistory(currentUser); + JSONArray actualMessageHistory = currentChat.getMessageHistory(currentUser); Assertions.assertNull(actualMessageHistory); } } diff --git a/backend/src/test/java/com/UoB/AILearningTool/DatabaseControllerTest.java b/backend/src/test/java/com/UoB/AILearningTool/DatabaseControllerTest.java index 0c024953..afc713af 100644 --- a/backend/src/test/java/com/UoB/AILearningTool/DatabaseControllerTest.java +++ b/backend/src/test/java/com/UoB/AILearningTool/DatabaseControllerTest.java @@ -1,103 +1,66 @@ package com.UoB.AILearningTool; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import com.UoB.AILearningTool.repository.ChatRepository; +import com.UoB.AILearningTool.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; -import java.util.ArrayList; +class DatabaseControllerTest { -public class DatabaseControllerTest { - @Test - @DisplayName("Check whether users can be created.") - public void createUsers() { - DatabaseController DBC = new DatabaseController(); - ArrayList usernames = new ArrayList<>(); - for (int i = 0; i < 20; i++) { - usernames.add(DBC.addUser(true)); - } + @Mock + private UserRepository userRepository; - for (String username : usernames) { - User user = DBC.getUser(username); - Assertions.assertEquals(username, user.getID()); - } - } - - @Test - @DisplayName("Check whether users can be deleted.") - public void deleteUsers() { - DatabaseController DBC = new DatabaseController(); - ArrayList usernames = new ArrayList<>(); - for (int i = 0; i < 20; i++) { - usernames.add(DBC.addUser(true)); - } - - // Deleting all users - for (String username : usernames) { - DBC.removeUser(username); - } - - // Search for non-existing user must return null - for (String username : usernames) { - Assertions.assertNull(DBC.getUser(username)); - } - } - - @Test - @DisplayName("Check whether chats can be created and accessed.") - public void createChats() { - DatabaseController DBC = new DatabaseController(); - ArrayList users = new ArrayList<>(); - ArrayList chatIDs = new ArrayList<>(); - - // Create users. - for (int i = 0; i < 20; i++) { - users.add(DBC.getUser(DBC.addUser(true))); - } - - // Create chats. - for (User user : users) { - chatIDs.add(DBC.createChat(user, "This is a first message.", "TEST")); - } - - // DBC.getChat() must return a non-null element. - for (int i = 0; i < 20; i++) { - Chat actualChat = DBC.getChat(users.get(i), chatIDs.get(i)); - Assertions.assertNotNull(actualChat); - } - } - - @Test - @DisplayName("Check whether chats can only be accessed by their owners.") - public void accessChatPermissionTest() { - Chat currentChat; - User currentUser; - DatabaseController DBC = new DatabaseController(); - ArrayList users = new ArrayList<>(); - ArrayList chatIDs = new ArrayList<>(); - - // Create users. - for (int i = 0; i < 20; i++) { - users.add(DBC.getUser(DBC.addUser(true))); - } + @Mock + private ChatRepository chatRepository; - // Create chats. - for (User user : users) { - chatIDs.add(DBC.createChat(user, "This is a first message.", "TEST")); - } + @InjectMocks + private DatabaseController databaseController; - for (int i = 0; i < 20; i++) { - // If currentUser is the chat owner, Chat object is returned. - currentUser = users.get(i); - currentChat = DBC.getChat(currentUser, chatIDs.get(i)); - Assertions.assertNotNull(currentChat); - for (int j = 0; j < 20; j++) { - if (i == j) {continue;} - // If currentUser isn't the chat owner, null object is returned. - currentUser = users.get(j); - currentChat = DBC.getChat(currentUser, chatIDs.get(i)); - Assertions.assertNull(currentChat); - } - } + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); } +// @Test +// void testAddUser() { +// UserEntity user = new UserEntity("testUser", "password123"); +// when(userRepository.save(any(UserEntity.class))).thenReturn(user); +// +// Boolean result = databaseController.addUser("testUser", "password123"); +// assertTrue(result); +// } +// +// @Test +// void testGetUser() { +// UserEntity user = new UserEntity("testUser", "password123"); +// when(userRepository.findById("testUser")).thenReturn(Optional.of(user)); +// +// UserEntity result = databaseController.getUser("testUser"); +// assertNotNull(result); +// assertEquals("testUser", result.getUsername()); +// } +// +// @Test +// void testRemoveUser() { +// when(userRepository.existsById("testUser")).thenReturn(true); +// doNothing().when(userRepository).deleteById("testUser"); +// +// boolean result = databaseController.removeUser("testUser"); +// assertTrue(result); +// } +// +// @Test +// void testCreateChat() { +// UserEntity user = new UserEntity("testUser", "password123"); +// when(userRepository.findById("testUser")).thenReturn(Optional.of(user)); +// +// ChatEntity chat = new ChatEntity(user, "Hello AI!", "mockSessionID"); +// when(chatRepository.save(any(ChatEntity.class))).thenReturn(chat); +// +// ChatEntity createdChat = databaseController.createChat("mockSessionID", "Hello AI!"); +// assertNotNull(createdChat); +// assertEquals("mockSessionID", createdChat.getSessionID()); +// } } diff --git a/backend/src/test/java/com/UoB/AILearningTool/OpenAIAPIControllerTest.java b/backend/src/test/java/com/UoB/AILearningTool/OpenAIAPIControllerTest.java index bf309cb2..1e761422 100644 --- a/backend/src/test/java/com/UoB/AILearningTool/OpenAIAPIControllerTest.java +++ b/backend/src/test/java/com/UoB/AILearningTool/OpenAIAPIControllerTest.java @@ -1,11 +1,11 @@ package com.UoB.AILearningTool; +import com.UoB.AILearningTool.model.ChatEntity; +import com.UoB.AILearningTool.model.UserEntity; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.util.ArrayList; - @DisplayName("Testing OpenAI API Controller for response handling") public class OpenAIAPIControllerTest { @@ -21,9 +21,8 @@ public void createThreadTest() { public void initialMessageHistoryRequestTest() { String newMessage = "I need help with finding online courses SkillsBuild."; OpenAIAPIController OAIC = new OpenAIAPIController(); - DatabaseController DBC = new DatabaseController(); - User user = DBC.getUser(DBC.addUser(true)); - Chat chat = DBC.getChat(user, DBC.createChat(user, newMessage, OAIC.createThread())); + UserEntity user = new UserEntity("testUserLogin1", "testUserPassword1"); + ChatEntity chat = new ChatEntity(user, "Hello there!", OAIC.createThread()); Integer response = OAIC.sendUserMessage(chat, newMessage); Assertions.assertEquals(200, response); diff --git a/backend/src/test/java/com/UoB/AILearningTool/SpringControllerTest.java b/backend/src/test/java/com/UoB/AILearningTool/SpringControllerTest.java index d5470a79..229bf014 100644 --- a/backend/src/test/java/com/UoB/AILearningTool/SpringControllerTest.java +++ b/backend/src/test/java/com/UoB/AILearningTool/SpringControllerTest.java @@ -1,143 +1,96 @@ package com.UoB.AILearningTool; -import org.json.JSONArray; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.Test; +import com.UoB.AILearningTool.model.UserEntity; +import com.UoB.AILearningTool.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.Cookie; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; -import java.io.IOException; -import java.io.PrintWriter; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -// integrates Mockito with JUnit @ExtendWith(MockitoExtension.class) class SpringControllerTest { @Mock - private DatabaseController mockDBController; - - @Mock - private OpenAIAPIController mockWXC; - - @Mock - private HttpServletResponse mockResponse; + private DatabaseController mockDBC; @Mock - private PrintWriter mockWriter; + private UserRepository userRepository; - // This Tells Mockito to inject the @Mock fields into SpringController's constructor @InjectMocks private SpringController springController; - @Captor - private ArgumentCaptor cookieCaptor; - - @BeforeEach - public void setUp() throws IOException { - Mockito.lenient().when(mockResponse.getWriter()).thenReturn(mockWriter); - } - - @Test - public void testNewUserCreated() { - - // mock addUser to return a specific ID "user123" when called wit optional consent = true - boolean optionalConsent = true; - String generatedUserID = "user123"; - when(mockDBController.addUser(optionalConsent)).thenReturn(generatedUserID); - - // call the signup method with the arranged parameters - springController.signup(optionalConsent, mockResponse); - - // verify that addUser was called with optionalConsent = true - verify(mockDBController, times(1)).addUser(optionalConsent); - - // capture the cookie added to the response - verify(mockResponse, times(1)).addCookie(cookieCaptor.capture()); - Cookie capturedCookie = cookieCaptor.getValue(); + private UserEntity testUser; - - // assertions on the captured cookie - assertEquals("userID", capturedCookie.getName(), "Cookie name should be userID"); - assertEquals(generatedUserID, capturedCookie.getValue(), "Cookie value should match generated userID"); - assertEquals(30 * 24 * 60 * 60, capturedCookie.getMaxAge(), "Cookie max age should be 30 days"); - } - - @Test - public void testRevokeConsent() { - - // define a userID "user123" to revoke consent for - String userID = "user123"; - - // mock removeUser to return true - when(mockDBController.removeUser(userID)).thenReturn(true); - - springController.revokeConsent(userID, mockResponse); - - // verify that removeUser was called with the correct userID - verify(mockDBController, times(1)).removeUser(userID); - - // verify that the status was set to 200 when user was removed - verify(mockResponse, times(1)).setStatus(HttpServletResponse.SC_OK); - - // capture the cookie added to the response - verify(mockResponse, times(1)).addCookie(cookieCaptor.capture()); - Cookie capturedCookie = cookieCaptor.getValue(); - - // assertions on captured cookie - assertEquals("userID", capturedCookie.getName(), "Cookie name should be userID"); - assertEquals("", capturedCookie.getValue(), "Cookie value should be empty"); - assertEquals(0, capturedCookie.getMaxAge(), "Cookie max age should be 0"); - } - -// TODO: No idea how this one works, refactor it for the new API structure. +// @BeforeEach +// public void setUp() throws Exception { +// MockitoAnnotations.openMocks(this); +// testUser = new UserEntity("testUser", "password123"); +// springController = new SpringController(mockDBC, null); +// +// Field userRepoField = SpringController.class.getDeclaredField("userRepository"); +// userRepoField.setAccessible(true); +// userRepoField.set(springController, userRepository); +// } +// // @Test -// void testCreateChat() { -// // Given -// String userID = "user123"; -// String initialMessage = "Hello chatbot!"; -// String generatedChatID = "abc123"; -// User mockUser = new User(true); -// Chat mockChat = mock(Chat.class); -// OpenAIAPIController OAIC = new OpenAIAPIController(); -// -// // Mock out DB calls -// when(mockDBController.getUser(userID)).thenReturn(mockUser); -// when(mockDBController.createChat(mockUser, initialMessage, OAIC.createThread())).thenReturn(generatedChatID); -// when(mockDBController.getChat(mockUser, generatedChatID)).thenReturn(mockChat); -// -// // Mock chat & AI calls -// JSONArray messageHistory = ; -// when(mockChat.getMessageHistory(mockUser)).thenReturn(messageHistory); -// -// WatsonxResponse aiResponse = new WatsonxResponse(200, "AI says hi"); -// when(mockWXC.sendUserMessage(mockChat, messageHistory)).thenReturn(aiResponse); -// -// // When -// springController.createChat(userID, initialMessage, mockResponse); -// -// // Then -// verify(mockDBController).getUser(userID); -// verify(mockDBController).createChat(mockUser, initialMessage); -// verify(mockDBController).getChat(mockUser, generatedChatID); -// -// verify(mockChat).getMessageHistory(mockUser); -// verify(mockWXC).sendUserMessage(messageHistory); -// -// // AI responded with status 200, so we expect addAIMessage to be called: -// verify(mockChat).addAIMessage(userID, "AI says hi"); -// -// // Finally, verify response -// verify(mockResponse).setContentType("text/plain"); -// verify(mockResponse).setStatus(200); -// verify(mockWriter).write(generatedChatID); +// void testRegisterUser_Success() { +// Map credentials = Map.of("username", "testUser", "password", "password123"); +// +// when(userRepository.existsById("testUser")).thenReturn(false); +// when(mockDBC.addUser("testUser", "password123", true)).thenReturn("testUser"); +// +// ResponseEntity> response = springController.registerUser(credentials); +// +// assertEquals(200, response.getStatusCodeValue()); +// assertTrue(response.getBody().containsKey("success")); +// +// verify(mockDBC, times(1)).addUser("testUser", "password123", true); +// } +// +// @Test +// void testRegisterUser_UsernameExists() { +// Map credentials = Map.of("username", "existingUser", "password", "password123"); +// +// when(userRepository.existsById("existingUser")).thenReturn(true); +// +// ResponseEntity> response = springController.registerUser(credentials); +// +// assertEquals(400, response.getStatusCodeValue()); +// assertTrue(response.getBody().containsKey("message")); +// assertEquals("Username already exists", response.getBody().get("message")); +// +// verify(mockDBC, never()).addUser(anyString(), anyString(), anyBoolean()); +// } +// +// @Test +// void testRevokeConsent_Success() { +// when(mockDBC.removeUser("testUser")).thenReturn(true); +// +// ResponseEntity> response = springController.revokeConsent("testUser"); +// +// assertEquals(200, response.getStatusCodeValue()); +// assertEquals("User data deleted successfully.", response.getBody().get("message")); +// +// verify(mockDBC, times(1)).removeUser("testUser"); +// } +// +// @Test +// void testRevokeConsent_Failure() { +// when(mockDBC.removeUser("testUser")).thenReturn(false); +// +// ResponseEntity> response = springController.revokeConsent("testUser"); +// +// assertEquals(400, response.getStatusCodeValue()); +// assertEquals("User not found", response.getBody().get("message")); +// +// verify(mockDBC, times(1)).removeUser("testUser"); // } - } - - - diff --git a/backend/src/test/java/com/UoB/AILearningTool/UserTest.java b/backend/src/test/java/com/UoB/AILearningTool/UserTest.java index c3cff525..25776171 100644 --- a/backend/src/test/java/com/UoB/AILearningTool/UserTest.java +++ b/backend/src/test/java/com/UoB/AILearningTool/UserTest.java @@ -1,22 +1,23 @@ package com.UoB.AILearningTool; +import com.UoB.AILearningTool.model.UserEntity; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; public class UserTest { + @Test @DisplayName("Checking whether 'User' constructor creates an object with optional consent correctly") public void correctFullConsentUserCreationTest() { - User user = new User(true); - Assertions.assertEquals(25, user.getID().length()); - Assertions.assertTrue(user.getOptionalConsent()); + UserEntity user = new UserEntity("testUserLogin1", "testUserPassword1"); + Assertions.assertEquals("testUserLogin1", user.getUsername()); } + @Test @DisplayName("Checking whether 'User' constructor creates an object without optional consent correctly") public void correctRequiredConsentUserCreationTest() { - User user = new User(false); - Assertions.assertEquals(25, user.getID().length()); - Assertions.assertFalse(user.getOptionalConsent()); + UserEntity user = new UserEntity("testUserLogin2", "testUserPassword2"); + Assertions.assertEquals("testUser2", user.getUsername()); } } diff --git a/docs/architecture_diagram.drawio b/docs/architecture_diagram.drawio index 0d199dcc..981c7320 100644 --- a/docs/architecture_diagram.drawio +++ b/docs/architecture_diagram.drawio @@ -1,13 +1,13 @@ - + - + - + @@ -43,13 +43,13 @@ - + - + - + diff --git a/docs/architecture_diagram.png b/docs/architecture_diagram.png index 88c3c49c..5bd3dcfa 100644 Binary files a/docs/architecture_diagram.png and b/docs/architecture_diagram.png differ diff --git a/docs/architecture_diagram.svg b/docs/architecture_diagram.svg index 4d013ee7..ed4b6f52 100644 --- a/docs/architecture_diagram.svg +++ b/docs/architecture_diagram.svg @@ -1,4 +1,4 @@ -
Browser
Browser
MariaDB
MariaDB
Database
Database
Vue 3
Vue 3
Frontend
Frontend
Spring boot
Spring boot
Java-based Backend
Java-based Backend
IBM Watsonx
IBM Watsonx
AI language model API
AI language model API
\ No newline at end of file +
Browser
Browser
SQLite
SQLite
Database
Database
Vue 3
Vue 3
Frontend
Frontend
Spring boot
Spring boot
Java-based Backend
Java-based Backend
OpenAI
OpenAI
AI language model API
AI language model API
\ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 39e96854..012b21f2 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -16,24 +16,12 @@