diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..72317128d Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index c64b53754..dc328da4f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,8 @@ dependency-reduced-pom.xml *.iml .log -/src/main/resources/static/built .classpath .factorypath .project .settings/ +uploads/posts/* diff --git a/pom.xml b/pom.xml index 32aeb8fc4..be80d1b09 100644 --- a/pom.xml +++ b/pom.xml @@ -1,35 +1,33 @@ - 4.0.0 + org.springframework.boot spring-boot-starter-parent 3.3.2 + com.makersacademy acebook-template 1.0-SNAPSHOT acebook Demo project for Spring Boot - - - - - - - - - - - - - + 21 + + + + org.springframework.boot + spring-boot-starter-mail + + org.springframework.boot spring-boot-starter-data-jpa @@ -42,6 +40,9 @@ org.springframework.boot spring-boot-starter-web + + + org.flywaydb flyway-core @@ -56,41 +57,50 @@ postgresql runtime + org.springframework.boot spring-boot-starter-test test + org.projectlombok lombok provided + junit junit test + org.seleniumhq.selenium selenium-java + 4.33.0 test + com.github.javafaker javafaker 0.15 test + com.okta.spring okta-spring-boot-starter 3.0.7 + org.springframework.boot spring-boot-starter-oauth2-client + org.thymeleaf.extras thymeleaf-extras-springsecurity6 @@ -108,8 +118,8 @@ maven-compiler-plugin 3.13.0 - 1.8 - 1.8 + 21 + 21 org.projectlombok diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 000000000..c332f9f22 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/main/.DS_Store b/src/main/.DS_Store new file mode 100644 index 000000000..3d9e7c3dc Binary files /dev/null and b/src/main/.DS_Store differ diff --git a/src/main/java/com/makersacademy/acebook/config/ContactMail.java b/src/main/java/com/makersacademy/acebook/config/ContactMail.java new file mode 100644 index 000000000..02a07d724 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/config/ContactMail.java @@ -0,0 +1,26 @@ +package com.makersacademy.acebook.config; + +import com.makersacademy.acebook.model.Contact; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +public class ContactMail { + + @Autowired + private JavaMailSender mailSender; + + public void sendEmail(Contact contact) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo("makersbnbgsai@gmail.com"); + message.setSubject("Acebook Support"); + message.setText( + "Name: " + contact.getName() + "\n" + + "Email: " + contact.getEmail() + "\n" + + "Message:\n" + contact.getMessage() + ); + mailSender.send(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/config/MvcConfiguration.java b/src/main/java/com/makersacademy/acebook/config/MvcConfiguration.java new file mode 100644 index 000000000..e67c1d264 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/config/MvcConfiguration.java @@ -0,0 +1,24 @@ +package com.makersacademy.acebook.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class MvcConfiguration implements WebMvcConfigurer { + + @Value("${upload.profile}") + private String profileUploadDir; + + @Value("${upload.posts}") + private String postsUploadDir; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/uploads/profile/**") + .addResourceLocations("file:" + profileUploadDir); + registry.addResourceHandler("/uploads/posts/**") + .addResourceLocations("file:" + postsUploadDir); + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/config/SecurityConfiguration.java b/src/main/java/com/makersacademy/acebook/config/SecurityConfiguration.java index 542bce54c..d65b3af91 100644 --- a/src/main/java/com/makersacademy/acebook/config/SecurityConfiguration.java +++ b/src/main/java/com/makersacademy/acebook/config/SecurityConfiguration.java @@ -1,5 +1,5 @@ package com.makersacademy.acebook.config; - +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -33,9 +33,12 @@ public SecurityFilterChain configure(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/", "/images/**").permitAll() + .requestMatchers("/welcome", "/contact", "/images/**").permitAll() .anyRequest().authenticated() + ) + + .oauth2Login(oauth2 -> oauth2 .successHandler(new AuthenticationSuccessHandler() { @Override @@ -46,15 +49,16 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo ) .logout(logout -> logout - .addLogoutHandler(logoutHandler())); + .addLogoutHandler(logoutHandler()) + .logoutSuccessUrl("/welcome")); // :point_left: Redirect target after logout); return http.build(); } private LogoutHandler logoutHandler() { return (request, response, authentication) -> { try { - String baseUrl = ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString(); - response.sendRedirect(issuer + "v2/logout?client_id=" + clientId + "&returnTo=" + baseUrl); +// String baseUrl = ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString(); + response.sendRedirect(issuer + "v2/logout?client_id=" + clientId + "&returnTo=" + "http://localhost:8080/welcome"); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/main/java/com/makersacademy/acebook/controller/FriendsController.java b/src/main/java/com/makersacademy/acebook/controller/FriendsController.java new file mode 100644 index 000000000..dca3864c6 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/FriendsController.java @@ -0,0 +1,157 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.model.FriendRequest; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.FriendRequestRepository; +import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.AuthenticatedUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +import java.util.Set; + +//SEE IF YOU CAN GET THE AUTHENTICATION METHOD TO REMOVE REPETITION!!!!1! + +@RestController +public class FriendsController { + @Autowired + UserRepository userRepository; + + @Autowired + FriendRequestRepository friendRequestRepository; + + @Autowired + AuthenticatedUserService authenticatedUserService; + + @GetMapping("/friends") + public ModelAndView viewFriends() { + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + String username = (String) principal.getAttributes().get("email"); + + // Get Authenticated User (logged-in user) + User currentUser = authenticatedUserService.getAuthenticatedUser(); + + // List of a user's friends + Set friends = currentUser.getFriends(); + + // Fetch incoming friend requests where the current user is the receiver and pending is true + Iterable incomingRequests = friendRequestRepository.findByReceiverAndPendingTrue(currentUser); + + ModelAndView mav = new ModelAndView("friends"); + mav.addObject("friends", friends); + mav.addObject("friendsCount", friends.size()); + mav.addObject("incomingRequests", incomingRequests); + return mav; + } + + + @PostMapping("/friend-requests/send") + public ModelAndView sendFriendRequest(@RequestParam("receiverId") Long receiverId) { + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + String currentUsername = (String) principal.getAttributes().get("email"); + + User sender = userRepository.findUserByUsername(currentUsername) + .orElseThrow(() -> new RuntimeException("User not found")); + + User receiver = userRepository.findById(receiverId) + .orElseThrow(() -> new RuntimeException("Receiver not found")); + + FriendRequest request = new FriendRequest(); + request.setSender(sender); + request.setReceiver(receiver); + request.setPending(true); + + friendRequestRepository.save(request); + + return new ModelAndView("redirect:/profile/" + receiverId); + } + + @PostMapping("/friend-requests/withdraw") + public ModelAndView withdrawFriendRequest(@RequestParam("receiverId") Long receiverId) { + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + String currentUsername = (String) principal.getAttributes().get("email"); + + User sender = userRepository.findUserByUsername(currentUsername) + .orElseThrow(() -> new RuntimeException("User not found")); + + User receiver = userRepository.findById(receiverId) + .orElseThrow(() -> new RuntimeException("Receiver not found")); + + FriendRequest request = friendRequestRepository + .findBySenderAndReceiverAndPendingTrue(sender, receiver); + + if (request != null) { + friendRequestRepository.delete(request); + } + + return new ModelAndView("redirect:/profile/" + receiverId); + } + + @PostMapping("/friend-requests/accept") + public ModelAndView acceptFriendRequest(@RequestParam Long requestId) { + FriendRequest request = friendRequestRepository.findById(requestId) + .orElseThrow(() -> new RuntimeException("Friend request not found")); + + User sender = request.getSender(); + User receiver = request.getReceiver(); + + sender.getFriends().add(receiver); + receiver.getFriends().add(sender); + + userRepository.save(sender); + userRepository.save(receiver); + + request.setPending(false); + friendRequestRepository.save(request); + + return new ModelAndView("redirect:/friends"); + } + + @PostMapping("/friend-requests/decline") + public ModelAndView declineFriendRequest(@RequestParam Long requestId) { + FriendRequest request = friendRequestRepository.findById(requestId) + .orElseThrow(() -> new RuntimeException("Friend request not found")); + + friendRequestRepository.delete(request); + + return new ModelAndView("redirect:/friends"); + } + + @PostMapping("/friends/remove") + public ModelAndView removeFriend(@RequestParam Long friendId) { + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + String currentUsername = (String) principal.getAttributes().get("email"); + + User currentUser = userRepository.findUserByUsername(currentUsername) + .orElseThrow(() -> new RuntimeException("User not found")); + + User friend = userRepository.findById(friendId) + .orElseThrow(() -> new RuntimeException("Friend not found")); + + currentUser.getFriends().remove(friend); + friend.getFriends().remove(currentUser); + + userRepository.save(currentUser); + userRepository.save(friend); + + return new ModelAndView("redirect:/friends"); + } +} diff --git a/src/main/java/com/makersacademy/acebook/controller/HomeController.java b/src/main/java/com/makersacademy/acebook/controller/HomeController.java index 2036ec7e0..1b11f9d69 100644 --- a/src/main/java/com/makersacademy/acebook/controller/HomeController.java +++ b/src/main/java/com/makersacademy/acebook/controller/HomeController.java @@ -1,13 +1,68 @@ package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.config.ContactMail; +import com.makersacademy.acebook.model.Contact; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.UserRepository; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.servlet.view.RedirectView; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; + +import java.security.Principal; + @Controller public class HomeController { - @RequestMapping(value = "/") - public RedirectView index() { - return new RedirectView("/posts"); - } + @GetMapping("/welcome") + public String welcome() { + return "welcome"; // This refers to the template "welcome.html" + } + + @GetMapping("/contact") + public String Contact() { return "contact"; + } + + // EMAIL FUNCTION + @Autowired + private ContactMail mail; + + @PostMapping("/contact") + public String submitContactForm(@ModelAttribute Contact contact) { + mail.sendEmail(contact); // call on the instance, NOT the class + return "redirect:/contact?success"; + } + + //Everything below is to do with entering name, surname and DOB after signup. + @Autowired + private UserRepository userRepository; + + + @GetMapping("/checkDetails") + public String checkDetails(Principal principal, Model model){ + String authID = principal.getName(); + User user = userRepository.findUserByAuthId(authID) + .orElseThrow(()-> new RuntimeException("User not Found")); + boolean isIncomplete = + (user.getForename() == null || user.getForename().isBlank()) || + (user.getSurname() == null || user.getSurname().isBlank()) || + (user.getDob() == null); + + if (isIncomplete){ + model.addAttribute(user); + return "completeDetails"; + } + return "redirect:/"; + } + + + } + + diff --git a/src/main/java/com/makersacademy/acebook/controller/LikeController.java b/src/main/java/com/makersacademy/acebook/controller/LikeController.java new file mode 100644 index 000000000..7694a692d --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/LikeController.java @@ -0,0 +1,49 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.model.Like; +import com.makersacademy.acebook.repository.LikeRepository; +import com.makersacademy.acebook.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.security.Principal; + +@Controller +public class LikeController { + + @Autowired + private LikeRepository likeRepository; + + @Autowired + private UserRepository userRepository; + + @PostMapping("/like") + public String likeItem( + Principal principal, + @RequestParam String likedType, + @RequestParam Long likedId, + @RequestParam String redirectUrl + ) { + String userEmail = principal.getName(); + Long userId = userRepository.findUserByAuthId(userEmail) + .orElseThrow(() -> new RuntimeException("User not found")) + .getId(); + + // prevent duplicates + boolean alreadyLiked = likeRepository + .findByUserIdAndLikedTypeAndLikedId(userId, likedType, likedId) + .isPresent(); + + if (!alreadyLiked) { + Like like = new Like(); + like.setUserId(userId); + like.setLikedType(likedType); + like.setLikedId(likedId); + likeRepository.save(like); + } + + return "redirect:" + redirectUrl; + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/controller/PostsController.java b/src/main/java/com/makersacademy/acebook/controller/PostsController.java index 57a7e5f4d..5a5bc79fc 100644 --- a/src/main/java/com/makersacademy/acebook/controller/PostsController.java +++ b/src/main/java/com/makersacademy/acebook/controller/PostsController.java @@ -1,32 +1,181 @@ package com.makersacademy.acebook.controller; +import com.makersacademy.acebook.model.Comment; import com.makersacademy.acebook.model.Post; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.CommentRepository; import com.makersacademy.acebook.repository.PostRepository; +import com.makersacademy.acebook.repository.LikeRepository; +import com.makersacademy.acebook.service.AuthenticatedUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.view.RedirectView; -import java.util.List; +import java.io.IOException; +import java.sql.SQLOutput; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; @Controller public class PostsController { @Autowired - PostRepository repository; + PostRepository postRepository; - @GetMapping("/posts") + @Autowired + CommentRepository commentRepository; // Make sure you have this + + @Autowired + UploadController uploadController; + + @Autowired + LikeRepository likeRepository; + + @Autowired + private AuthenticatedUserService authenticatedUserService; + + @GetMapping("/globalfeed") public String index(Model model) { - Iterable posts = repository.findAll(); - model.addAttribute("posts", posts); + List recentSortedPosts = StreamSupport.stream(postRepository.findAll().spliterator(), false) + .filter(post -> post.getTimeStamp() != null && + post.getTimeStamp().isAfter(LocalDateTime.now().minusSeconds(3000))) + .sorted(Comparator.comparing(Post::getTimeStamp).reversed()) + .toList(); + + List comments = (List) commentRepository.findAll(); + Map> commentsByPostId = comments.stream() + .filter(comment -> comment.getTimeStamp() != null && + comment.getTimeStamp().isAfter(LocalDateTime.now().minusSeconds(600))) + .sorted(Comparator.comparing(Comment::getTimeStamp).reversed()) + .collect(Collectors.groupingBy(comment -> comment.getPostID().longValue())); + + model.addAttribute("commentsByPostId", commentsByPostId); + model.addAttribute("posts", recentSortedPosts); model.addAttribute("post", new Post()); - return "posts/index"; + + // For posts + Map postLikeCounts = new HashMap<>(); + for (Post post : recentSortedPosts) { + long count = likeRepository.countByLikedTypeAndLikedId("post", post.getId()); + postLikeCounts.put(post.getId(), count); + } + model.addAttribute("likeCountsByPostId", postLikeCounts); + + // For comments + Map commentLikeCounts = new HashMap<>(); + for (Comment comment : comments) { + long count = likeRepository.countByLikedTypeAndLikedId("comment", comment.getId()); + commentLikeCounts.put(comment.getId(), count); + } + model.addAttribute("likeCountsByCommentId", commentLikeCounts); + + // Adding attribute comments + model.addAttribute("comments", comments); + model.addAttribute("comment", new Comment()); + + // Adding attribute current user + User currentUser = authenticatedUserService.getAuthenticatedUser(); + model.addAttribute("current_user", currentUser); + + // Adding attribute friends + Set friends = currentUser.getFriends(); + model.addAttribute("friends", friends); + + return "posts/globalfeed"; } @PostMapping("/posts") - public RedirectView create(@ModelAttribute Post post) { - repository.save(post); - return new RedirectView("/posts"); + public RedirectView createPost(@ModelAttribute Post post, + @RequestParam(value = "image", required = false)MultipartFile image, + @RequestParam String username, @RequestParam Integer user_id, + @RequestParam String forename, @RequestParam String surname) throws IOException { + post.setTimeStamp(LocalDateTime.now()); + post.setUsername(username); + post.setUserID(user_id); + post.setForename(forename); + post.setSurname(surname); + + + Post savedPost = postRepository.save(post); + + if (image != null && !image.isEmpty()) { + uploadController.handlePostImageUpload(savedPost.getId(), image); + } + + return new RedirectView("/"); + } + + @PostMapping("/comment") + public RedirectView createComment(@ModelAttribute Comment comment, + @RequestParam String username, + @RequestParam Integer postID) { + comment.setTimeStamp(LocalDateTime.now()); + comment.setUsername(username); + comment.setPostID(postID); + commentRepository.save(comment); + return new RedirectView("/"); + } + + @GetMapping("/") + public String feed(Model model) { +// List recentSortedPosts = StreamSupport.stream(postRepository.findAll().spliterator(), false) +// .filter(post -> post.getTimeStamp() != null && +// post.getTimeStamp().isAfter(LocalDateTime.now().minusSeconds(3000))) +// .sorted(Comparator.comparing(Post::getTimeStamp).reversed()) +// .toList(); + + List comments = (List) commentRepository.findAll(); + Map> commentsByPostId = comments.stream() + .filter(comment -> comment.getTimeStamp() != null && + comment.getTimeStamp().isAfter(LocalDateTime.now().minusSeconds(600))) + .sorted(Comparator.comparing(Comment::getTimeStamp).reversed()) + .collect(Collectors.groupingBy(comment -> comment.getPostID().longValue())); + + model.addAttribute("commentsByPostId", commentsByPostId); + + Long myId = authenticatedUserService.getAuthenticatedUser().getId(); + List feedPosts = postRepository.findFeedNative(myId).stream() + .filter(post -> post.getTimeStamp() != null && + post.getTimeStamp().isAfter(LocalDateTime.now().minusSeconds(3000))) + .sorted(Comparator.comparing(Post::getTimeStamp).reversed()) + .toList(); + + model.addAttribute("posts", feedPosts); + model.addAttribute("post", new Post()); + + model.addAttribute("comments", comments); + model.addAttribute("comment", new Comment()); + + User currentUser = authenticatedUserService.getAuthenticatedUser(); + model.addAttribute("current_user", currentUser); + + Set friends = currentUser.getFriends(); + model.addAttribute("friends", friends); + + // like counts for posts + Map likeCountsByPostId = new HashMap<>(); + for (Post post : feedPosts) { + long count = likeRepository.countByLikedTypeAndLikedId("post", post.getId()); + likeCountsByPostId.put(post.getId(), count); + } + model.addAttribute("likeCountsByPostId", likeCountsByPostId); + + // likes count for comments + Map likeCountsByCommentId = new HashMap<>(); + for (Comment comment : comments) { + long count = likeRepository.countByLikedTypeAndLikedId("comment", comment.getId()); + likeCountsByCommentId.put(comment.getId(), count); + } + model.addAttribute("likeCountsByCommentId", likeCountsByCommentId); + + return "posts/friendsfeed"; } } diff --git a/src/main/java/com/makersacademy/acebook/controller/ProfileController.java b/src/main/java/com/makersacademy/acebook/controller/ProfileController.java new file mode 100644 index 000000000..cee7479f0 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/ProfileController.java @@ -0,0 +1,102 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.FriendRequestRepository; +import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.AuthenticatedUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Controller +public class ProfileController { + + @Autowired + private UserRepository userRepository; + + @Autowired + private AuthenticatedUserService authenticatedUserService; + + @Autowired + private FriendRequestRepository friendRequestRepository; + + @GetMapping("/myProfile") + public String showMyProfile(Model model) { + User user = authenticatedUserService.getAuthenticatedUser(); + model.addAttribute("user", user); + + // Test data for navigating between profiles (optional) + Iterable users = userRepository.findAll(); + model.addAttribute("users", users); + + return "myProfile"; + } + + @GetMapping("/profile/{id}") + public String showUserProfile(@PathVariable Long id, Model model) { + User profileUser = userRepository.findUserById(id) + .orElseThrow(() -> new RuntimeException("User not found")); + + User currentUser = authenticatedUserService.getAuthenticatedUser(); + + boolean isAlreadyFriends = currentUser.getFriends().contains(profileUser); + boolean hasPendingRequest = friendRequestRepository + .existsBySenderAndReceiverAndPendingTrue(currentUser, profileUser); + boolean isSelfProfile = currentUser.equals(profileUser); + + model.addAttribute("user", profileUser); + model.addAttribute("isAlreadyFriends", isAlreadyFriends); + model.addAttribute("hasPendingRequest", hasPendingRequest); + model.addAttribute("isSelfProfile", isSelfProfile); + + // Adding attribute friends + Set currentUserFriends = currentUser.getFriends(); + model.addAttribute("friends", currentUserFriends); + + // Adding attribute friends + Set profileUserFriends = profileUser.getFriends(); + model.addAttribute("profileFriends", profileUserFriends); + + // Adding limited friends + List friendList = new ArrayList<>(currentUserFriends); + Collections.shuffle(friendList); + List limitedFriends = friendList.stream() + .limit(4) + .collect(Collectors.toList()); + model.addAttribute("friends", limitedFriends); + + return "profile"; + } + + @PostMapping("/myProfile") + public String updateUserProfile(@ModelAttribute User updatedUser, + @RequestParam(name = "field") String field) { + User user = authenticatedUserService.getAuthenticatedUser(); + System.out.println("==============="); + System.out.println(updatedUser.getSurname()); + switch (field) { + case "forename" -> user.setForename(updatedUser.getForename()); + case "surname" -> user.setSurname(updatedUser.getSurname()); + case "description" -> user.setDescription(updatedUser.getDescription()); + case "gender" -> user.setGender(updatedUser.getGender()); + case "pronouns" -> user.setPronouns(updatedUser.getPronouns()); + case "current_city" -> user.setCurrentCity(updatedUser.getCurrentCity()); + case "hometown" -> user.setHometown(updatedUser.getHometown()); + case "job" -> user.setJob(updatedUser.getJob()); + case "school" -> user.setSchool(updatedUser.getSchool()); + case "relationship_status" -> user.setRelationshipStatus(updatedUser.getRelationshipStatus()); + case "sexual_orientation" -> user.setSexualOrientation(updatedUser.getSexualOrientation()); + case "political_views" -> user.setPoliticalViews(updatedUser.getPoliticalViews()); + case "religion" -> user.setReligion(updatedUser.getReligion()); + } + userRepository.save(user); + return "redirect:/myProfile"; + } +} diff --git a/src/main/java/com/makersacademy/acebook/controller/UploadController.java b/src/main/java/com/makersacademy/acebook/controller/UploadController.java new file mode 100644 index 000000000..b99bf2d39 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/UploadController.java @@ -0,0 +1,143 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.model.Post; +import com.makersacademy.acebook.repository.PostRepository; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.AuthenticatedUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.stereotype.Controller; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +@Controller +public class UploadController { + + + @Value("${upload.profile}") + String profileUploadDir; + + @Value("${upload.posts}") + String postsUploadDir; + + @Autowired + UserRepository userRepository; + + @Autowired + PostRepository postRepository; + + @Autowired + AuthenticatedUserService authenticatedUserService; + + @PostMapping("/uploadProfileImage") + public String handleProfileImageUpload( + @RequestParam("image") MultipartFile image) + throws IOException { + + if (image.isEmpty()) { + return "redirect:/myProfile?error=empty"; + } + + // Use AuthenticatedUserService to get the current authenticated user + User user = authenticatedUserService.getAuthenticatedUser(); + + // Rename file: authId.extension + String extension = getFileExtension(image.getOriginalFilename()); + String sanitizedId = user.getAuthId().replaceAll("[^a-zA-Z0-9_-]", "_"); + String profileFileName = sanitizedId + "." + extension; + + + // Create upload directory if it doesn't exist + Path uploadPath = Paths.get(profileUploadDir); + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + + // Delete old image if it exists + if (user.getProfile_image_src() != null && !user.getProfile_image_src().isEmpty()) { + String oldFileName = user.getProfile_image_src().replace("/uploads/profile/", ""); + Path oldImagePath = uploadPath.resolve(oldFileName); + try { + Files.deleteIfExists(oldImagePath); + } catch (IOException e) { + // Log the error but continue with new upload + System.err.println("Failed to delete old profile image: " + e.getMessage()); + } + } + + // Save new image + Path destination = uploadPath.resolve(profileFileName); + image.transferTo(destination.toFile()); + + // Update user with new image path (web-accessible path) + user.setProfile_image_src("/uploads/profile/" + profileFileName); + userRepository.save(user); + + return "redirect:/myProfile"; + } + + @PostMapping("/uploadPostImage") + public String handlePostImageUpload( + @RequestParam("postId") Long postId, + @RequestParam("image") MultipartFile image) + throws IOException { + + if (image.isEmpty()) { + return "redirect:/myProfile?error=empty"; + } + + // Post Repository redirect if post is NULL + Post savedPost = postRepository.findById(postId).orElse(null); + if (savedPost == null) { + return "redirect:/myProfile?error=notfound"; + } + + // Sanitize timestamp (e.g., "2024-06-09T15:30:00" => "20240609_153000") + String safeTimestamp = savedPost.getFormattedTimestamp() + .replaceAll("[:\\-T]", "") + .replaceAll("\\s+", "_"); + + // Rename file: post_postId.extension + String extension = getFileExtension(image.getOriginalFilename()); + String postFileName = "post_" + savedPost.getId() + "_" + safeTimestamp + "." + extension; + + // Create upload directory if it doesn't exist + Path uploadPath = Paths.get(postsUploadDir); + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + + // Delete old image if it exists + if (savedPost.getPost_image_src() != null && !savedPost.getPost_image_src().isEmpty()) { + String oldFileName = savedPost.getPost_image_src().replace("/uploads/posts/", ""); + Path oldImagePath = uploadPath.resolve(oldFileName); + try { + Files.deleteIfExists(oldImagePath); + } catch (IOException e) { + // Log the error but continue with new upload + System.err.println("Failed to delete old posts image: " + e.getMessage()); + } + } + + // Save new image + Path destination = uploadPath.resolve(postFileName); + image.transferTo(destination.toFile()); + + // Save post_image_src to repository + savedPost.setPost_image_src("/uploads/posts/" + postFileName); + postRepository.save(savedPost); + return "redirect:/"; + } + private String getFileExtension(String filename) { + int dotIndex = filename.lastIndexOf("."); + return (dotIndex != -1) ? filename.substring(dotIndex + 1) : ""; + } +} diff --git a/src/main/java/com/makersacademy/acebook/controller/UsersController.java b/src/main/java/com/makersacademy/acebook/controller/UsersController.java index a7c9db1d8..42d6e876d 100644 --- a/src/main/java/com/makersacademy/acebook/controller/UsersController.java +++ b/src/main/java/com/makersacademy/acebook/controller/UsersController.java @@ -2,29 +2,33 @@ import com.makersacademy.acebook.model.User; import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.AuthenticatedUserService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.view.RedirectView; @RestController public class UsersController { @Autowired - UserRepository userRepository; + AuthenticatedUserService authenticatedUserService; @GetMapping("/users/after-login") public RedirectView afterLogin() { - DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder - .getContext() - .getAuthentication() - .getPrincipal(); + authenticatedUserService.getAuthenticatedUser(); + return new RedirectView("/checkDetails"); + } - String username = (String) principal.getAttributes().get("email"); - userRepository - .findUserByUsername(username) - .orElseGet(() -> userRepository.save(new User(username))); + //used for saving the details of new users after sign up. + @Autowired + UserRepository userRepository; + @PostMapping("/saveNewUserDetails") + public RedirectView saveNewUserDetails(@ModelAttribute User newUser){ + User user = authenticatedUserService.getAuthenticatedUser(); + user.setForename(newUser.getForename()); + user.setSurname(newUser.getSurname()); + user.setDob(newUser.getDob()); + userRepository.save(user); - return new RedirectView("/posts"); + return new RedirectView("/"); } -} +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/model/Comment.java b/src/main/java/com/makersacademy/acebook/model/Comment.java new file mode 100644 index 000000000..75e5d4d6e --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Comment.java @@ -0,0 +1,41 @@ +package com.makersacademy.acebook.model; + +import jakarta.persistence.*; +import lombok.Data; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Data + @Entity + @Table(name = "comments") + + +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + + private Long id; + private String username; + @Column(name = "comment") + private String commentContent; + private Integer postID; + @Column(name = "time_stamp", updatable = false) + private LocalDateTime timeStamp; + + + public Comment() {} + + public Comment(String username, String commentContent, + Integer postID, LocalDateTime timeStamp) { + this.username = username; + this.commentContent = commentContent; + this.postID = postID; + this.timeStamp = timeStamp; + } + public String getFormattedTimestamp() { + if (timeStamp == null) return "No Time Stamp"; + return timeStamp.format(DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm")); + + } +} diff --git a/src/main/java/com/makersacademy/acebook/model/Contact.java b/src/main/java/com/makersacademy/acebook/model/Contact.java new file mode 100644 index 000000000..45fd60038 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Contact.java @@ -0,0 +1,11 @@ +package com.makersacademy.acebook.model; + +import lombok.Data; + +@Data +public class Contact { + private String name; + private String email; + private String message; + +} diff --git a/src/main/java/com/makersacademy/acebook/model/FriendRequest.java b/src/main/java/com/makersacademy/acebook/model/FriendRequest.java new file mode 100644 index 000000000..c15e18fc1 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/FriendRequest.java @@ -0,0 +1,37 @@ +package com.makersacademy.acebook.model; + +import jakarta.persistence.*; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@Entity +@Table(name = "FRIEND_REQUESTS") +public class FriendRequest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "sender_id", nullable = false) + private User sender; + + @ManyToOne + @JoinColumn(name = "receiver_id", nullable = false) + private User receiver; + + private boolean pending; + + @Column(name = "sent_at") + private LocalDateTime sentAt; + + public FriendRequest() {} + + public FriendRequest(User sender, User receiver, boolean pending) { + this.sender = sender; + this.receiver = receiver; + this.pending = pending; + } +} diff --git a/src/main/java/com/makersacademy/acebook/model/Like.java b/src/main/java/com/makersacademy/acebook/model/Like.java new file mode 100644 index 000000000..3419b8aae --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Like.java @@ -0,0 +1,37 @@ +package com.makersacademy.acebook.model; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import lombok.Data; + +@Data +@Entity +@Table(name = "likes", uniqueConstraints = { + @UniqueConstraint(columnNames = {"user_id", "liked_type", "liked_id"}) +}) +public class Like { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "liked_type", nullable = false) + private String likedType; + + @Column(name = "liked_id", nullable = false) + private Long likedId; + + @Column(name = "created_at") + private LocalDateTime createdAt = LocalDateTime.now(); + + public Like() {} + + public Like(Long userId, String likedType, Long likedId) { + this.userId = userId; + this.likedType = likedType; + this.likedId = likedId; + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/model/Post.java b/src/main/java/com/makersacademy/acebook/model/Post.java index 33492c6b1..9a966236f 100644 --- a/src/main/java/com/makersacademy/acebook/model/Post.java +++ b/src/main/java/com/makersacademy/acebook/model/Post.java @@ -4,6 +4,9 @@ import lombok.Data; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + @Data @Entity @Table(name = "POSTS") @@ -13,10 +16,32 @@ public class Post { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String content; + private String username; + private LocalDateTime timeStamp; + private String post_image_src; + private Integer user_id; + private String forename; + private String surname; + public Post() {} - public Post(String content) { + public Post(String content, String username, LocalDateTime timeStamp, String post_image_src, Integer user_id, String forename, String surname) { this.content = content; + this.username = username; + this.post_image_src = post_image_src; + this.timeStamp = timeStamp; + this.user_id = user_id; + this.forename = forename; + this.surname = surname; + } + public String getFormattedTimestamp() { + if (timeStamp == null) return "No Time Stamp"; + return timeStamp.format(DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm")); + + } + + public void setUserID(Integer userId) { } } + diff --git a/src/main/java/com/makersacademy/acebook/model/User.java b/src/main/java/com/makersacademy/acebook/model/User.java index 6013fbe23..fc4854fa7 100644 --- a/src/main/java/com/makersacademy/acebook/model/User.java +++ b/src/main/java/com/makersacademy/acebook/model/User.java @@ -1,11 +1,19 @@ package com.makersacademy.acebook.model; import jakarta.persistence.*; -import lombok.Data; +import lombok.Getter; +import lombok.Setter; -import static java.lang.Boolean.TRUE; +import java.sql.Date; +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; -@Data +//If @Data is required later, add annotations separately to avoid issues: +//@Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor + +@Getter +@Setter @Entity @Table(name = "USERS") public class User { @@ -15,17 +23,74 @@ public class User { private String username; private boolean enabled; + // need to use @Column to tell JPA to map this field to the database as it has a "unique" qualifier + @Column(name = "auth0_id", unique = true) + private String authId; + + // need to tell JPA that this column is TEXT as it auto assumes it will be VARCHAR if it's a string + @Column(columnDefinition = "TEXT") + private String description; + + private String forename; + private String surname; + private String profile_image_src; + private String gender; + private String pronouns; + private String currentCity; + private String hometown; + private String job; + private String school; + private String relationshipStatus; + private String sexualOrientation; + private String politicalViews; + private String religion; + + //Added dob to store the date of birth as a LocalTime which is of format YYYY-MM-DD same as SQL DATE + private LocalDate dob; + + // Tells the friends table which values to use + @ManyToMany + @JoinTable( + name = "friends", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "friend_id") + ) + + // Using hashset to store friends to prevent duplicates and lets us do faster access/search + private Set friends = new HashSet<>(); + + // no arguments constructor public User() { - this.enabled = TRUE; } - public User(String username) { + // constructor for login (extracts username, auth0_id) + // all other fields set to null until updated by user + public User(String username, boolean enabled, String authId) { this.username = username; - this.enabled = TRUE; + this.authId = authId; + this.enabled = enabled; } + // constructor with username and enabled, for testing (no auth0_id required) public User(String username, boolean enabled) { this.username = username; this.enabled = enabled; } + + // full constructor, all arguments + public User(String username, boolean enabled, String authId, String description, String forename, String surname, String profile_image_src) { + this.username = username; + this.authId = authId; + this.description = null; + this.enabled = enabled; + } + + public User(String username, boolean enabled, String authId, String description) { + this.username = username; + this.authId = authId; + this.description = description; + this.forename = forename; + this.surname = surname; + this.profile_image_src = profile_image_src; + } } diff --git a/src/main/java/com/makersacademy/acebook/repository/CommentRepository.java b/src/main/java/com/makersacademy/acebook/repository/CommentRepository.java new file mode 100644 index 000000000..8375e213e --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/CommentRepository.java @@ -0,0 +1,13 @@ +package com.makersacademy.acebook.repository; + +import com.makersacademy.acebook.model.Comment; +import com.makersacademy.acebook.model.User; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface CommentRepository extends CrudRepository { + public Optional findCommentByUsername(String username); + public Optional findCommentByPostID(Integer postID); + +} diff --git a/src/main/java/com/makersacademy/acebook/repository/FriendRequestRepository.java b/src/main/java/com/makersacademy/acebook/repository/FriendRequestRepository.java new file mode 100644 index 000000000..f38a44a94 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/FriendRequestRepository.java @@ -0,0 +1,12 @@ +package com.makersacademy.acebook.repository; + +import com.makersacademy.acebook.model.FriendRequest; +import com.makersacademy.acebook.model.User; +import org.springframework.data.repository.CrudRepository; + +public interface FriendRequestRepository extends CrudRepository { + boolean existsBySenderAndReceiverAndPendingTrue(User sender, User receiver); + FriendRequest findBySenderAndReceiverAndPendingTrue(User sender, User receiver); + Iterable findByReceiverAndPendingTrue(User receiver); + FriendRequest findBySenderAndReceiverAndPendingFalse(User sender, User receiver); +} diff --git a/src/main/java/com/makersacademy/acebook/repository/LikeRepository.java b/src/main/java/com/makersacademy/acebook/repository/LikeRepository.java new file mode 100644 index 000000000..6676572a9 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/LikeRepository.java @@ -0,0 +1,11 @@ +package com.makersacademy.acebook.repository; + +import com.makersacademy.acebook.model.Like; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface LikeRepository extends CrudRepository { + Optional findByUserIdAndLikedTypeAndLikedId(Long userId, String likedType, Long likedId); + long countByLikedTypeAndLikedId(String likedType, Long likedId); +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/repository/PostRepository.java b/src/main/java/com/makersacademy/acebook/repository/PostRepository.java index d435e0ce1..beb8982ab 100644 --- a/src/main/java/com/makersacademy/acebook/repository/PostRepository.java +++ b/src/main/java/com/makersacademy/acebook/repository/PostRepository.java @@ -2,8 +2,26 @@ import com.makersacademy.acebook.model.Post; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; -public interface PostRepository extends CrudRepository { +import java.util.List; +public interface PostRepository extends CrudRepository { + @Query( + value = """ + SELECT p.* + FROM posts p + WHERE p.user_id = :currentUserId + OR p.user_id IN ( + SELECT friend_id + FROM friends + WHERE user_id = :currentUserId + ) + """, + nativeQuery = true + ) + List findFeedNative(@Param("currentUserId") Long currentUserId); } + diff --git a/src/main/java/com/makersacademy/acebook/repository/UserRepository.java b/src/main/java/com/makersacademy/acebook/repository/UserRepository.java index 4b4fd2b52..cf73dfe45 100644 --- a/src/main/java/com/makersacademy/acebook/repository/UserRepository.java +++ b/src/main/java/com/makersacademy/acebook/repository/UserRepository.java @@ -7,4 +7,6 @@ public interface UserRepository extends CrudRepository { public Optional findUserByUsername(String username); -} + public Optional findUserByAuthId(String auth0Id); + public Optional findUserById(Long id); +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/service/AuthenticatedUserService.java b/src/main/java/com/makersacademy/acebook/service/AuthenticatedUserService.java new file mode 100644 index 000000000..31b3ff07b --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/AuthenticatedUserService.java @@ -0,0 +1,55 @@ +package com.makersacademy.acebook.service; + +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.UserRepository; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +@Service +public class AuthenticatedUserService { + + private final UserRepository userRepository; + + public AuthenticatedUserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + // Helper method to get the OAuth2User principal or throw if unexpected + private OAuth2User getPrincipal() { + Object principalObj = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (!(principalObj instanceof OAuth2User principal)) { + throw new RuntimeException("Unexpected principal type: " + principalObj.getClass().getName()); + } + return principal; + } + + + public User getAuthenticatedUser() { + String authId = getPrincipal().getName(); + User user = userRepository.findUserByAuthId(authId) + .orElseGet(() -> { + System.out.println("User not found, creating new User with authId: " + authId); + return createNewUser(authId); + }); + return user; + } + + private User createNewUser(String authId) { + OAuth2User principal = getPrincipal(); + String email = (String) principal.getAttributes().get("email"); + User newUser = new User(); + newUser.setUsername(email); + newUser.setAuthId(authId); + newUser.setEnabled(true); + return userRepository.save(newUser); + } + + public String getAuthenticatedAuthId() { + return getPrincipal().getName(); + } + + public String getAuthenticatedEmail() { + return (String) getPrincipal().getAttributes().get("email"); + } +} diff --git a/src/main/java/com/makersacademy/acebook/service/FriendRequestService.java b/src/main/java/com/makersacademy/acebook/service/FriendRequestService.java new file mode 100644 index 000000000..d48d05a6a --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/FriendRequestService.java @@ -0,0 +1,29 @@ +package com.makersacademy.acebook.service; + +import com.makersacademy.acebook.model.FriendRequest; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.FriendRequestRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +public class FriendRequestService { + @Autowired + private FriendRequestRepository friendRequestRepo; + + public void sendFriendRequest(User sender, User receiver) { + // Avoid duplicates, custom CRUD method in repository + if (friendRequestRepo.existsBySenderAndReceiverAndPendingTrue(sender, receiver)) return; + + FriendRequest request = new FriendRequest(); + request.setSender(sender); + request.setReceiver(receiver); + request.setPending(true); + request.setSentAt(LocalDateTime.now()); + + friendRequestRepo.save(request); + } +} + diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index fd2cad503..3a8872370 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -1,7 +1,9 @@ spring.datasource.url=jdbc:postgresql://localhost:5432/acebook_springboot_development spring.datasource.username= spring.datasource.password= +spring.datasource.driver-class-name=org.postgresql.Driver flyway.baseline-on-migrate=true -spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false +spring.jpa.properties.hibernate.boot.allow_jdbc_metadata_access=false spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.open-in-view=true diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 865b41e1c..6f666e852 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -3,4 +3,6 @@ spring.datasource.username= spring.datasource.password= flyway.baseline-on-migrate=true spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +upload.profile=target/test-uploads/profile +upload.posts=target/test-uploads/posts \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7b2ed1a6b..3f964f147 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,7 +2,16 @@ spring.profiles.active=dev spring.data.rest.base-path=/api spring.datasource.platform=postgres spring.jpa.hibernate.ddl-auto=validate +spring.servlet.multipart.max-file-size=10MB +spring.servlet.multipart.max-request-size=10MB logging.level.org.springframework.web=DEBUG logging.level.org.springframework.security=DEBUG logging.level.org.springframework.web.client.RestTemplate=DEBUG logging.level.org.apache.http=DEBUG + +spring.mail.host=smtp.gmail.com +spring.mail.port=587 +spring.mail.username=makersbnbgsai@gmail.com +spring.mail.password=${EMAIL_PASS_ACEBOOK} +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 699e8575a..e4bb51154 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,5 +1,22 @@ okta: oauth2: - issuer: https://dev-edward-andress.uk.auth0.com/ - client-id: ${OKTA_CLIENT_ID} - client-secret: ${OKTA_CLIENT_SECRET} + issuer: ${ISSUER_ACEBOOK} + client-id: ${CLIENT_ID_ACEBOOK} + client-secret: ${CLIENT_SECRET_ACEBOOK} + +upload: + profile: ${UPLOAD_PROFILE_PATH} + posts: ${UPLOAD_POSTS_PATH} + +spring: + mail: + host: smtp.gmail.com + port: 587 + username: makersbnbgsai@gmail.com + password: ${EMAIL_PASS_ACEBOOK} + properties: + mail: + smtp: + auth: true + starttls: + enable: true \ No newline at end of file diff --git a/src/main/resources/db/migration/V10__adding_comments_database.sql b/src/main/resources/db/migration/V10__adding_comments_database.sql new file mode 100644 index 000000000..caac0e922 --- /dev/null +++ b/src/main/resources/db/migration/V10__adding_comments_database.sql @@ -0,0 +1,10 @@ +CREATE TABLE comments( +id BIGSERIAL PRIMARY KEY, +username VARCHAR(255), +comment TEXT, +postID INT, +time_stamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, +FOREIGN KEY (postID) REFERENCES posts(id) +); + +INSERT INTO comments (username, comment, postID) VALUES('Test Comment User','First test comment', 1); diff --git a/src/main/resources/db/migration/V11__change_comment_definition.sql b/src/main/resources/db/migration/V11__change_comment_definition.sql new file mode 100644 index 000000000..a86060d7b --- /dev/null +++ b/src/main/resources/db/migration/V11__change_comment_definition.sql @@ -0,0 +1,2 @@ +ALTER TABLE comments +ALTER COLUMN time_stamp SET DEFAULT CURRENT_TIMESTAMP; \ No newline at end of file diff --git a/src/main/resources/db/migration/V12__add_user_timestamp.sql b/src/main/resources/db/migration/V12__add_user_timestamp.sql new file mode 100644 index 000000000..e863a389d --- /dev/null +++ b/src/main/resources/db/migration/V12__add_user_timestamp.sql @@ -0,0 +1,2 @@ +ALTER TABLE posts +ADD COLUMN time_stamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP; \ No newline at end of file diff --git a/src/main/resources/db/migration/V13__add_cols_and_update_seed_data_in_user_table.sql b/src/main/resources/db/migration/V13__add_cols_and_update_seed_data_in_user_table.sql new file mode 100644 index 000000000..deeaf11b9 --- /dev/null +++ b/src/main/resources/db/migration/V13__add_cols_and_update_seed_data_in_user_table.sql @@ -0,0 +1,129 @@ +ALTER TABLE users +ADD COLUMN gender VARCHAR(50), +ADD COLUMN pronouns VARCHAR(50), +ADD COLUMN current_city VARCHAR(100), +ADD COLUMN hometown VARCHAR(100), +ADD COLUMN job VARCHAR(100), +ADD COLUMN school VARCHAR(100), +ADD COLUMN relationship_status VARCHAR(50), +ADD COLUMN sexual_orientation VARCHAR(50), +ADD COLUMN political_views VARCHAR(100), +ADD COLUMN religion VARCHAR(100); + +UPDATE users SET + gender = 'Male', + pronouns = 'he/him', + current_city = 'Los Angeles', + hometown = 'Philadelphia', + job = 'Software Tester', + school = 'MIT', + relationship_status = 'Married', + sexual_orientation = 'Heterosexual', + political_views = 'Independent', + religion = 'Agnostic' +WHERE username = 'test@test.com'; + +UPDATE users SET + gender = 'Female', + pronouns = 'she/her', + current_city = 'Nashville', + hometown = 'Reading', + job = 'Singer-Songwriter', + school = 'Hendersonville High School', + relationship_status = 'Single', + sexual_orientation = 'Heterosexual', + political_views = 'Centrist', + religion = 'Christian' +WHERE username = 't.swiftie89@popvibes.com'; + +UPDATE users SET + gender = 'Male', + pronouns = 'he/him', + current_city = 'Malibu', + hometown = 'New York City', + job = 'Actor', + school = 'Santa Monica High School', + relationship_status = 'Married', + sexual_orientation = 'Heterosexual', + political_views = 'Liberal', + religion = 'Jewish' +WHERE username = 'robert.downey@starkmail.com'; + +UPDATE users SET + gender = 'Female', + pronouns = 'she/her', + current_city = 'Houston', + hometown = 'Houston', + job = 'Entrepreneur / Artist', + school = 'High School for the Performing and Visual Arts', + relationship_status = 'Married', + sexual_orientation = 'Heterosexual', + political_views = 'Progressive', + religion = 'Christian' +WHERE username = 'bey.queenb@lemonadehub.net'; + +UPDATE users SET + gender = 'Male', + pronouns = 'he/him', + current_city = 'Los Angeles', + hometown = 'Hayward', + job = 'Actor / Producer', + school = 'University of Miami', + relationship_status = 'Married', + sexual_orientation = 'Heterosexual', + political_views = 'Moderate', + religion = 'Christian' +WHERE username = 'the.rock@smashmail.org'; + +UPDATE users SET + gender = 'Female', + pronouns = 'she/her', + current_city = 'London', + hometown = 'Paris', + job = 'Actress / Activist', + school = 'Brown University', + relationship_status = 'Single', + sexual_orientation = 'Heterosexual', + political_views = 'Progressive', + religion = 'Spiritual' +WHERE username = 'emma.watson.books@hermione.io'; + +UPDATE users SET + gender = 'Male', + pronouns = 'he/him', + current_city = 'Toronto', + hometown = 'Toronto', + job = 'Musician / Entrepreneur', + school = 'Forest Hill Collegiate Institute', + relationship_status = 'Single', + sexual_orientation = 'Heterosexual', + political_views = 'Liberal', + religion = 'Christian' +WHERE username = 'drake@octobersveryown.ca'; + +UPDATE users SET + gender = 'Female', + pronouns = 'she/her', + current_city = 'Bridgetown', + hometown = 'Bridgetown', + job = 'Singer / CEO', + school = 'Combermere School', + relationship_status = 'In a relationship', + sexual_orientation = 'Bisexual', + political_views = 'Liberal', + religion = 'Christian' +WHERE username = 'rih.navy@fentyhq.co'; + +UPDATE users SET + gender = 'Female', + pronouns = 'she/her', + current_city = 'Los Angeles', + hometown = 'Oakland', + job = 'Actress / Model', + school = 'Oakland School for the Arts', + relationship_status = 'In a relationship', + sexual_orientation = 'Queer', + political_views = 'Progressive', + religion = 'Non-religious' +WHERE username = 'zendaya.now@webslings.tv'; + diff --git a/src/main/resources/db/migration/V14__adding_DOB_field_to_users.sql b/src/main/resources/db/migration/V14__adding_DOB_field_to_users.sql new file mode 100644 index 000000000..3b11debe9 --- /dev/null +++ b/src/main/resources/db/migration/V14__adding_DOB_field_to_users.sql @@ -0,0 +1,2 @@ + ALTER TABLE users + ADD dob DATE; \ No newline at end of file diff --git a/src/main/resources/db/migration/V15__add_likes_table.sql b/src/main/resources/db/migration/V15__add_likes_table.sql new file mode 100644 index 000000000..9149faa38 --- /dev/null +++ b/src/main/resources/db/migration/V15__add_likes_table.sql @@ -0,0 +1,9 @@ +CREATE TABLE likes ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + liked_type VARCHAR(20) NOT NULL CHECK (liked_type IN ('post', 'comment')), + liked_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + UNIQUE (user_id, liked_type, liked_id) +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V16__populate_famous_user_profile_images.sql b/src/main/resources/db/migration/V16__populate_famous_user_profile_images.sql new file mode 100644 index 000000000..5e93a3b4b --- /dev/null +++ b/src/main/resources/db/migration/V16__populate_famous_user_profile_images.sql @@ -0,0 +1,8 @@ +UPDATE users SET profile_image_src = '/uploads/profile/taylor_swift.jpg' WHERE username = 't.swiftie89@popvibes.com'; +UPDATE users SET profile_image_src = '/uploads/profile/robertdj.jpg' WHERE username = 'robert.downey@starkmail.com'; +UPDATE users SET profile_image_src = '/uploads/profile/beyonce.jpg' WHERE username = 'bey.queenb@lemonadehub.net'; +UPDATE users SET profile_image_src = '/uploads/profile/therock.jpg' WHERE username = 'the.rock@smashmail.org'; +UPDATE users SET profile_image_src = '/uploads/profile/emmawatson.jpg' WHERE username = 'emma.watson.books@hermione.io'; +UPDATE users SET profile_image_src = '/uploads/profile/drake.jpg' WHERE username = 'drake@octobersveryown.ca'; +UPDATE users SET profile_image_src = '/uploads/profile/rihanna.jpg' WHERE username = 'rih.navy@fentyhq.co'; +UPDATE users SET profile_image_src = '/uploads/profile/zendaya.jpg' WHERE username = 'zendaya.now@webslings.tv'; diff --git a/src/main/resources/db/migration/V17__create_user_id_column_in_posts.sql b/src/main/resources/db/migration/V17__create_user_id_column_in_posts.sql new file mode 100644 index 000000000..10547a2c5 --- /dev/null +++ b/src/main/resources/db/migration/V17__create_user_id_column_in_posts.sql @@ -0,0 +1,3 @@ +ALTER TABLE posts + ADD COLUMN user_id INTEGER +; diff --git a/src/main/resources/db/migration/V18__create_forename_surname_in_posts.sql b/src/main/resources/db/migration/V18__create_forename_surname_in_posts.sql new file mode 100644 index 000000000..ab0729b07 --- /dev/null +++ b/src/main/resources/db/migration/V18__create_forename_surname_in_posts.sql @@ -0,0 +1,4 @@ +ALTER TABLE posts + ADD COLUMN forename TEXT, + ADD COLUMN surname TEXT +; diff --git a/src/main/resources/db/migration/V3__create_user_column.sql b/src/main/resources/db/migration/V3__create_user_column.sql new file mode 100644 index 000000000..568548409 --- /dev/null +++ b/src/main/resources/db/migration/V3__create_user_column.sql @@ -0,0 +1,4 @@ +ALTER TABLE posts + ADD COLUMN username TEXT +; +INSERT INTO posts (username, content) VALUES('Test Post User', 'First test post'); \ No newline at end of file diff --git a/src/main/resources/db/migration/V4__create_friends_table.sql b/src/main/resources/db/migration/V4__create_friends_table.sql new file mode 100644 index 000000000..c999c5fd8 --- /dev/null +++ b/src/main/resources/db/migration/V4__create_friends_table.sql @@ -0,0 +1,10 @@ +CREATE TABLE friends ( + user_id bigint NOT NULL, + friend_id bigint NOT NULL, + PRIMARY KEY (user_id, friend_id), + + -- These constraints force the tables to use only existing user ids. + -- On delete cascade deletes friendships if either user or friend deletes their account. + CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT fk_friend FOREIGN KEY (friend_id) REFERENCES users(id) ON DELETE CASCADE +); diff --git a/src/main/resources/db/migration/V5__add_cols_to_user_table.sql b/src/main/resources/db/migration/V5__add_cols_to_user_table.sql new file mode 100644 index 000000000..4c84b6ac5 --- /dev/null +++ b/src/main/resources/db/migration/V5__add_cols_to_user_table.sql @@ -0,0 +1,3 @@ +ALTER TABLE users +ADD COLUMN auth0_id VARCHAR(255) UNIQUE, +ADD COLUMN description TEXT; \ No newline at end of file diff --git a/src/main/resources/db/migration/V6__add_cols_to_users_table.sql b/src/main/resources/db/migration/V6__add_cols_to_users_table.sql new file mode 100644 index 000000000..19759c5eb --- /dev/null +++ b/src/main/resources/db/migration/V6__add_cols_to_users_table.sql @@ -0,0 +1,3 @@ +ALTER TABLE users +ADD COLUMN forename VARCHAR(255), +ADD COLUMN surname VARCHAR(255); \ No newline at end of file diff --git a/src/main/resources/db/migration/V7__add_image_src_to_users_table.sql b/src/main/resources/db/migration/V7__add_image_src_to_users_table.sql new file mode 100644 index 000000000..1f263fc03 --- /dev/null +++ b/src/main/resources/db/migration/V7__add_image_src_to_users_table.sql @@ -0,0 +1,5 @@ +ALTER TABLE users +ADD COLUMN profile_image_src VARCHAR(255); + +ALTER TABLE posts +ADD COLUMN post_image_src VARCHAR(255); \ No newline at end of file diff --git a/src/main/resources/db/migration/V8__add_sample_users.sql b/src/main/resources/db/migration/V8__add_sample_users.sql new file mode 100644 index 000000000..b78fdbca8 --- /dev/null +++ b/src/main/resources/db/migration/V8__add_sample_users.sql @@ -0,0 +1,90 @@ +INSERT INTO users (username, enabled, auth0_id, description, forename, surname) +VALUES ('test@test.com', + 'TRUE', + 'auth0|6846adb55e1667fca8f00822', + 'test description', + 'John', + 'Smith' ), + + ('t.swiftie89@popvibes.com', + 'TRUE', + NULL, + 'writing songs in lowercase & living eras one heartbreak at a time.', + 'Taylor', + 'Swift' ), + + ('robert.downey@starkmail.com', + 'TRUE', + NULL, + 'genius, billionaire, occasional Avenger—still figuring out email folders.', + 'Robert', + 'Downey'), + + ('bey.queenb@lemonadehub.net', + 'TRUE', + NULL, + 'building empires in heels & harmony.', + 'Beyoncé', + 'Knowles'), + + ('the.rock@smashmail.org', + 'TRUE', + NULL, + 'lifting heavy things, making big moves, staying humble.', + 'Dwayne', + 'Johnson'), + + ('emma.watson.books@hermione.io', + 'TRUE', + NULL, + 'actress, activist, bookworm—powered by caffeine and curiosity.', + 'Emma', + 'Watson'), + + ('drake@octobersveryown.ca', + 'TRUE', + NULL, + 'emotional in the booth, iced out in the booth, still reading your texts.', + 'Aubrey "Drake"', + 'Graham'), + + ('rih.navy@fentyhq.co', + 'TRUE', + NULL, + 'CEO energy with a side of island breeze.', + 'Robyn "Rihanna"', + 'Fenty'), + + ('zendaya.now@webslings.tv', + 'TRUE', + NULL, + 'actor, fashion nerd, part-time superhero, full-time cozy.', + 'Zendaya', + 'Coleman'); + +INSERT INTO friends (user_id, friend_id) +VALUES + ( + (SELECT id FROM users WHERE username = 'test@test.com'), + (SELECT id FROM users WHERE username = 't.swiftie89@popvibes.com') + ), + ( + (SELECT id FROM users WHERE username = 'test@test.com'), + (SELECT id FROM users WHERE username = 'robert.downey@starkmail.com') + ), + ( + (SELECT id FROM users WHERE username = 'test@test.com'), + (SELECT id FROM users WHERE username = 'bey.queenb@lemonadehub.net') + ), + ( + (SELECT id FROM users WHERE username = 'test@test.com'), + (SELECT id FROM users WHERE username = 'the.rock@smashmail.org') + ), + ( + (SELECT id FROM users WHERE username = 'test@test.com'), + (SELECT id FROM users WHERE username = 'emma.watson.books@hermione.io') + ), + ( + (SELECT id FROM users WHERE username = 'test@test.com'), + (SELECT id FROM users WHERE username = 'drake@octobersveryown.ca') + ); \ No newline at end of file diff --git a/src/main/resources/db/migration/V9__create_friend_requests_tabl.sql b/src/main/resources/db/migration/V9__create_friend_requests_tabl.sql new file mode 100644 index 000000000..a7ea7a20d --- /dev/null +++ b/src/main/resources/db/migration/V9__create_friend_requests_tabl.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS friend_requests; + +CREATE TABLE friend_requests ( + id bigserial PRIMARY KEY, + sender_id bigint NOT NULL, + receiver_id bigint NOT NULL, + pending boolean NOT NULL DEFAULT TRUE, + sent_at TIMESTAMP, + CONSTRAINT fk_sender FOREIGN KEY (sender_id) REFERENCES users(id), + CONSTRAINT fk_receiver FOREIGN KEY (receiver_id) REFERENCES users(id) +); \ No newline at end of file diff --git a/src/main/resources/static/assets/attach-paper-clip-thin-line-flat-color-icon-linear-illustration-pictogram-isolated-on-white-background-colorful-long-shadow-design-free-vector.jpg b/src/main/resources/static/assets/attach-paper-clip-thin-line-flat-color-icon-linear-illustration-pictogram-isolated-on-white-background-colorful-long-shadow-design-free-vector.jpg new file mode 100644 index 000000000..580200f83 Binary files /dev/null and b/src/main/resources/static/assets/attach-paper-clip-thin-line-flat-color-icon-linear-illustration-pictogram-isolated-on-white-background-colorful-long-shadow-design-free-vector.jpg differ diff --git a/src/main/resources/static/assets/attachment.png b/src/main/resources/static/assets/attachment.png new file mode 100644 index 000000000..b3835bd91 Binary files /dev/null and b/src/main/resources/static/assets/attachment.png differ diff --git a/src/main/resources/static/assets/default-profile.png b/src/main/resources/static/assets/default-profile.png new file mode 100644 index 000000000..09892098a Binary files /dev/null and b/src/main/resources/static/assets/default-profile.png differ diff --git a/src/main/resources/static/main.css b/src/main/resources/static/main.css index d0260873c..b9e32b042 100644 --- a/src/main/resources/static/main.css +++ b/src/main/resources/static/main.css @@ -1,10 +1,749 @@ -.posts-main { - border-collapse: collapse; +.acebook-logo { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-size: 2.5rem; + font-weight: bold; + color: #1877f2; + text-decoration: none; + letter-spacing: -1.5px; } -.post-main { - border: 1px solid #999; - padding: 0.5rem; - text-align: left; - margin-bottom: 0.5rem;; +body { + background-color: #f6faff; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + margin: 0; + padding: 0; + color: #000; + display: flex; + flex-direction: column; + min-height: 100vh; } + +.layout-container { + display: flex; + min-height: calc(100vh - 120px); /* Adjust based on header/footer height */ + width: 100%; + gap: 1.5rem; + padding: 1.5rem; + box-sizing: border-box; +} + +.left-sidebar, .right-sidebar { + flex: 0 0 300px; + display: flex; + flex-direction: column; +} + +.main-content { + width: 100%; + max-width: none; + flex: 1; + margin: 0; + padding: 0; +} + +.main-inner { + flex: 1; /* Takes remaining space */ + min-width: 0; /* Prevents overflow issues */ +} + +.center-content { + flex: 1; + min-width: 0; +} + +.sidebar-box { + background-color: #dce8fb; + padding: 1.5rem; + border-radius: 12px; + border: 2px solid #6c9fd8; + flex: 1; /* Makes sidebar boxes fill available height */ + margin-top: 0; /* Aligns with post form */ +} + +/* Friends sidebar styling */ +.sidebar-title { + color: #1877f2; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid #e4e6eb; + text-align: center; +} + +.friends-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.friend-card { + background: white; + border-radius: 10px; + padding: 10px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +.friend-card:hover { + background: #f0f2f5; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.friend-link { + display: flex; + align-items: center; + text-decoration: none; + color: #050505; +} + +.friend-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + overflow: hidden; + margin-right: 10px; + border: 2px solid #e4e6eb; +} + +.friend-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.friend-info { + display: flex; + flex-direction: column; +} + +.friend-name { + font-weight: 600; + font-size: 14px; +} + +.friend-status { + font-size: 12px; + color: #65676b; +} + +/* Ensure the post form aligns with sidebars */ +.post-form { + margin-top: 0; +} + +/* Responsive adjustments */ +@media (max-width: 900px) { + .post-form-container { + flex-direction: column; + gap: 1rem; + } + .profile-col { + flex-direction: row; + align-items: center; + justify-content: flex-start; + max-width: none; + } + .profile-grid { + grid-template-columns: 1fr; + grid-template-rows: auto auto auto auto; + } + .profile-picture-container { + max-width: 400px; + margin: 0 auto; + } + .profile-picture-container { grid-row: 1; grid-column: 1; } + .profile-banner-container { grid-row: 2; grid-column: 1; } + .profile-info-container { grid-row: 3; grid-column: 1; } + .profile-content-container { grid-row: 4; grid-column: 1; } + .user-name-block { + text-align: left; + margin-left: 1rem; + } + .layout-container { + flex-direction: column; + min-height: auto; + } + .friends-grid { + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 1rem; + } +} + +.left-sidebar, .right-sidebar { + flex: 0 0 22%; /* or 20%, or whatever "more room" means for you */ + max-width: 22%; /* prevents them from growing too large */ + min-width: 180px; /* optional: prevents being too small on small screens */ + display: flex; + flex-direction: column; +} + +.sidebar-box { + margin-bottom: 1.5rem; +} + + +.post-form { + background-color: #dce8fb; + padding: 1.5em; + border-radius: 12px; + border: 2px solid #6c9fd8; + margin-bottom: 2em; + width: 100%; + box-sizing: border-box; +} + +.post-form-container { + display: flex; + flex-direction: row; + gap: 2rem; /* Space between left and right */ + align-items: flex-start; +} + +.profile-col { + display: flex; + flex-direction: column; + align-items: center; + min-width: 100px; + max-width: 130px; + flex-shrink: 0; + gap: 0.5rem; +} + + +.profile-picture-circle { + width: 56px; + height: 56px; + border-radius: 50%; + overflow: hidden; + border: 2px solid #e4e6eb; + margin-bottom: 0.5rem; +} + +.profile-picture-circle img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.profile-and-name-row { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 0.5rem; +} + +.user-name-block { + text-align: center; +} + +.user-forename, .user-surname { + font-weight: 600; + font-size: 1.1rem; + color: #222; +} + +.composer-col { + flex: 1 1 0; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.post-content-area { + flex-grow: 1; +} + +.post-textarea { + width: 100%; + min-height: 90px; + padding: 1rem; + border-radius: 8px; + border: 1px solid #ddd; + font-family: inherit; + resize: vertical; + box-sizing: border-box; +} + +.post-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: auto; + width: 100%; + position: relative; +} + +.image-upload-label { + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + color: #555; + font-weight: 500; +} + +.image-upload-label:hover { + color: #1877f2; +} + +.image-upload-icon { + width: 24px; + height: 24px; +} + +.image-upload-input { + display: none; +} + +.post-submit-button { + margin-left: auto; /* Pushes the button to the far right */ + background-color: #1877f2; + color: white; + border: none; + padding: 0.5rem 1.5rem; + border-radius: 6px; + font-weight: bold; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + transition: background-color 0.2s; +} + +.post-submit-button:hover { + background-color: #166fe5; +} + +.post-submit-icon { + width: 16px; + height: 16px; +} + +.post-list { + list-style-type: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 1.5em; /* Adds space between posts */ +} + +.post-item { + background-color: #dce8fb; + padding: 1.5em; + border-radius: 12px; + border: 2px solid #6c9fd8; + margin: 0; /* Reset margin */ + box-shadow: 0 2px 4px rgba(0,0,0,0.1); /* Optional: adds subtle shadow */ +} + +.post-container { + margin-bottom: 0; /* Remove margin-bottom from post-container */ + padding: 0; /* Remove padding from post-container */ + background-color: transparent; /* Make background transparent */ + border: none; /* Remove border */ +} + +.post-header { + display: flex; + justify-content: space-between; + margin-bottom: 0.5em; + align-items: center; +} + +#post-send { + width: 100%; + box-sizing: border-box; + padding: 0.8em; + margin-bottom: 0.5em; +} + +.comment-form { + background-color: #dce8fb; + padding: 1em; + border-radius: 10px; + border: 1px solid #a3c1ec; + margin-top: 1em; + display: flex; + flex-wrap: wrap; + gap: 0.5em; + align-items: center; +} + +#comment-send { + flex-grow: 1; + min-width: 200px; + padding: 0.6em; +} +.comment-submit-button { + background-color: #1877f2; + color: white; + border: none; + padding: 0.5rem 1.5rem; + border-radius: 6px; + font-weight: bold; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + transition: background-color 0.2s; +} +.comment-list { + background-color: #eef5ff; + padding: 0.8em; + border-radius: 10px; + border: 1px solid #a3c1ec; + margin-top: 1em; + list-style-type: none; + padding-left: 0; +} + +.comment-item { + margin-bottom: 0.8em; + padding-bottom: 0.8em; + border-bottom: 1px solid #d0e0ff; + list-style-type: none; + margin-left: 0; +} + +.comment-item:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.post-image-section { + margin-top: 1em; + padding: 1em; + background-color: #e6f0ff; + border-radius: 10px; + border: 1px solid #a3c1ec; +} + +.post-image-upload { + display: flex; + flex-direction: column; + gap: 0.8em; + margin-bottom: 1em; +} + +.post-image-display img { + max-width: 100%; + max-height: 540px; /* Good for social feeds */ + width: auto; + height: auto; + object-fit: contain; /* Show full image, no cropping */ + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0,0,0,0.07); + display: block; + margin: 0 auto; +} + +hr { + display: none; /* Hide hr since we're using gap for spacing */ +} + +.profile-container { + display: grid; + grid-template-columns: 260px 1.5fr 2.5fr 1fr; + gap: 1.5rem; + max-width: 1400px; + margin: 2rem auto; + padding: 0 1rem; +} + + +/* Style the center two columns as one unified “blue” background area */ +.profile-main { + flex: 1; + grid-column: 2 / span 2; + display: grid; + grid-template-rows: auto auto; + gap: 1.5rem; + background: #dce8fb; /* light blue wrapper */ + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 6px rgba(24,119,242,0.1); +} + +.profile-grid { + display: grid; + grid-template-columns: 1fr 2fr; + grid-template-rows: auto auto; + gap: 1.5rem; + margin: 1.5rem; +} + +/* Top row: picture card & banner card side-by-side */ +.profile-top { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 1.5rem; +} + +.profile-picture-container { + grid-row: 1; + grid-column: 1; + background: #b0b0b0; + border-radius: 12px; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 1/1; /* Maintain square aspect ratio */ +} + +.profile-banner-container { + grid-row: 1; + grid-column: 2; + background: #b0b0b0; + border-radius: 12px; + overflow: hidden; + min-height: 300px; + display: flex; + align-items: center; + justify-content: center; +} + +/* The grey “Profile Picture” card */ +.profile-picture-card { + background: #b0b0b0; + border-radius: 12px; + padding: 1rem; /* add padding around the image */ + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 6px rgba(24,119,242,0.1); +} + +.profile-picture { + width: 400px; /* smaller width */ + height: 400px; /* smaller height */ + border-radius: 12px; + object-fit: cover; +} + +/* The grey “Banner Image” card */ +.profile-banner { + background: #b0b0b0; + border-radius: 12px; + min-height: 700px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; +} + +.profile-nav-card { + background: #e4eefe; + border: 1px solid #a3c1ec; + border-radius: 12px; + padding: 1rem; + margin-top: 1.5rem; +} + +/* The button row under the banner */ +.profile-buttons { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.profile-buttons button { + flex: 1; + min-width: 100px; + background: #fff; + border: 1px solid #6c9fd8; + border-radius: 8px; + padding: 0.75rem; + cursor: pointer; + transition: all 0.2s; + font-weight: 600; +} + +.profile-buttons button:hover { + background: #c7daf7; + transform: translateY(-2px); +} + +/* Bottom row: details column + content column */ +.profile-details-section { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 1.5rem; +} + +/* Left details panel */ +.profile-info-box { + background: #e4eefe; + border-radius: 12px; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.profile-info-container { + grid-row: 2; + grid-column: 1; + background: #e4eefe; + border-radius: 12px; + padding: 1.5rem; + border: 1px solid #a3c1ec; +} + +/* Each detail line as its own pill */ +.profile-details-box { + background: white; + border: 1px solid #a3c1ec; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; +} + +/* Bio box taller */ +.profile-bio { + background: white; + border: 1px solid #a3c1ec; + border-radius: 8px; + padding: 1rem; + min-height: 150px; +} + +/* Right content panel */ +.profile-content { + background: #b7d3f7; + border-radius: 10px; + padding: 1.5rem; + box-shadow: inset 0 0 10px rgba(24,119,242,0.1); + min-height: 300px; +} + +.profile-content-container { + grid-row: 2; + grid-column: 2; + background: #b7d3f7; + border-radius: 12px; + padding: 1.5rem; +} + +.detail-label { + font-weight: bold; + color: #1877f2; + margin-bottom: 0.3rem; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.detail-value { + font-size: 1rem; + color: #333; + line-height: 1.4; +} + +.content-section { + display: none; + padding: 1.5rem; + background: #dce8fb; + border-radius: 10px; + margin-top: 1.5rem; +} + +.content-section.active-section { + display: block; +} + +.nav-button.active { + background-color: #1877f2; + color: white; + border-color: #1877f2; +} + +.profile-friends-section { + padding: 1.5rem; + background: #f0f2f5; + border-radius: 12px; + margin-top: 1.5rem; +} + +.profile-friends-title { + color: #1877f2; + margin-bottom: 1.5rem; + font-size: 1.5rem; + border-bottom: 2px solid #e4e6eb; + padding-bottom: 0.5rem; +} + +.profile-friends-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 1.5rem; +} + +.profile-friends-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 1.5rem; +} + +.profile-friend-card:hover { + transform: translateY(-5px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15); +} + +.profile-friend-link { + display: flex; + flex-direction: column; + text-decoration: none; + color: inherit; + height: 100%; +} + +.profile-friend-avatar-container { + position: relative; + padding-top: 100%; /* 1:1 Aspect Ratio */ +} + +.profile-friend-avatar { + width: 100%; + height: 100%; +} + +.profile-friend-info { + padding: 1rem; + text-align: center; +} + +.profile-friend-name { + display: block; + font-weight: 600; + margin-bottom: 0.25rem; + color: #050505; +} + +.profile-friend-status { + display: flex; + align-items: center; + justify-content: center; + font-size: 0.85rem; + color: #65676b; + gap: 0.25rem; +} + +.profile-status-indicator { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background: #31a24c; +} + +.profile-status-indicator.active { + background: #31a24c; +} + +.profile-status-indicator.offline { + background: #ddd; +} \ No newline at end of file diff --git a/src/main/resources/templates/Fragments.html b/src/main/resources/templates/Fragments.html new file mode 100644 index 000000000..a4bc30570 --- /dev/null +++ b/src/main/resources/templates/Fragments.html @@ -0,0 +1,122 @@ + + + +
+
+
+ + + + + +
+
+ Logged in as +
+ + Logout + +
+ +
+ + + +
+
+ + +
+
+ Contact Us +
+ © 2023 Acebook. All rights reserved. +
+
+
+ + + + + + +
+

Posts

+
+
+
+
+

About

+ +
+ +
+
+ +
+ +
+ +
+
+

Photos

+
+
diff --git a/src/main/resources/templates/completeDetails.html b/src/main/resources/templates/completeDetails.html new file mode 100644 index 000000000..063f677e9 --- /dev/null +++ b/src/main/resources/templates/completeDetails.html @@ -0,0 +1,28 @@ + + + + + + Details + + +
+
+
+ Kindly enter all your details so we can give you more "personalised" ads ;) + + +
+ + +
+ + +
+
+ +
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/contact.html b/src/main/resources/templates/contact.html new file mode 100644 index 000000000..821da4537 --- /dev/null +++ b/src/main/resources/templates/contact.html @@ -0,0 +1,41 @@ + + + + + Contact + + + + +
+ +

Contact Us

+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/friends.html b/src/main/resources/templates/friends.html new file mode 100644 index 000000000..ceff0558c --- /dev/null +++ b/src/main/resources/templates/friends.html @@ -0,0 +1,40 @@ + + + + + Friends + + + +
+ + +

Friends

+
    +
  • + +
  • +
+ +

Friend Requests

+
    +
  • + +
    + + +
    +
    + + +
    +
  • +
+

You currently have no friend requests.

+ + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/myProfile.html b/src/main/resources/templates/myProfile.html new file mode 100644 index 000000000..d9f1c2eab --- /dev/null +++ b/src/main/resources/templates/myProfile.html @@ -0,0 +1,146 @@ + + + + User Profile + + + +
+ +

User Profile

+ +

Username: Email address

+

Description: Description

+

Forename: Forename

+

Surname: Surname

+

Gender: Gender

+

Pronouns: Pronouns

+

Current City: Current City

+

Hometown: Hometown

+

Job: Job

+

School: School

+

Relationship Status: Relationship Status

+

Sexual Orientation: Sexual Orientation

+

Political Views: Political Views

+

Religion: Religion

+ + +
+

This is an example of how clicking your friends' usernames will return their profiles:

+ + + +
+
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ + +
+ + + + +
+ + +
+

Current Profile Image:

+ Profile Image +
+ + +
+ + diff --git a/src/main/resources/templates/posts/friendsfeed.html b/src/main/resources/templates/posts/friendsfeed.html new file mode 100644 index 000000000..5f6f24dd6 --- /dev/null +++ b/src/main/resources/templates/posts/friendsfeed.html @@ -0,0 +1,167 @@ + + + + + + Friends Feed + + + +
+ +
+ +
+ + + + + +
+

Friends Feed

+ + + +
+
+ +
+ Profile picture +
+ +
+ + + +
+ + + + + +
+
+
+ + + + + + + +
+ + +
    +
  • +
    +
    + + +
    + +

    + +
    + +
    + Post Image +
    +
    + + +
    +
    + + + + + + +
    +
    + +
    + + +
      +
    • +
      + commented at: +
      + + + + +
      +
      + + + + + + +
      +
      + +
    • +
    + + +
    + + + + + +
    + +
  • +
+
+ + + + +
+ +
+
+ + + + diff --git a/src/main/resources/templates/posts/globalfeed.html b/src/main/resources/templates/posts/globalfeed.html new file mode 100644 index 000000000..d10796def --- /dev/null +++ b/src/main/resources/templates/posts/globalfeed.html @@ -0,0 +1,118 @@ + + + + + + Acebook + + + +
+ +
+ +
+ + +
+ +
+

acebook feed

+

Go to your friends feed

+ + + +
+
+ +
+
+ Profile picture +
+
+ Forename + Surname +
+
+ +
+ +
+ + +
+
+
+ + + + + + + +
+ + +
    +
  • +
    +
    + + +
    + +

    + +
    + +
    + Post Image +
    +
    + +
    + + +
      +
    • +
      + commented at: +
      + +
    • +
    + + +
    + + + + + +
    + +
  • +
+
+ +
+ +
+ +
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/posts/index.html b/src/main/resources/templates/posts/index.html deleted file mode 100644 index b5ef169f1..000000000 --- a/src/main/resources/templates/posts/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - Acebook - - - - -

Posts

- -
- Signed in as -
- -
-

Content:

-

-
- -
    -
  • -
- - - diff --git a/src/main/resources/templates/profile.html b/src/main/resources/templates/profile.html new file mode 100644 index 000000000..286b11cb8 --- /dev/null +++ b/src/main/resources/templates/profile.html @@ -0,0 +1,148 @@ + + + + + User Profile + + + + + + + +
+ + +
+ +
+ + +
+
+
+ +
+ Profile Picture +
+ + +
+
+ BANNER IMAGE +
+
+ + +
+
+
Name
+
+
+ +
+
Gender
+
+
+ +
+
Pronouns
+
+
+ +
+
Occupation
+
+
+ +
+
Education
+
+
+ +
+
Location
+
+
+ +
+
About Me
+
+
+
+ + + + +
+ +
+
+ + + + +
+
+ +
+ +

Timeline

+
+
+ +
+ +

About

+
+
+ +
+ +

Friends

+
+
+ +
+ +

Photos

+
+
+
+
+
+
+ + +
+
+ + +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/welcome.html b/src/main/resources/templates/welcome.html new file mode 100644 index 000000000..27dcd9425 --- /dev/null +++ b/src/main/resources/templates/welcome.html @@ -0,0 +1,12 @@ + + + + Welcome + + +
+ +

Welcome to Acebook, please log in here

+
+ + \ No newline at end of file diff --git a/src/test/java/com/makersacademy/acebook/controller/UploadControllerIntegrationTest.java b/src/test/java/com/makersacademy/acebook/controller/UploadControllerIntegrationTest.java new file mode 100644 index 000000000..5427c104f --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/controller/UploadControllerIntegrationTest.java @@ -0,0 +1,85 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.AuthenticatedUserService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class UploadControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private AuthenticatedUserService authenticatedUserService; + + @MockBean + private UserRepository userRepository; + + @Value("${upload.profile}") + private String profileUploadDir; + + private User testUser; + + @BeforeEach + void setUp() { + // Create user without builder + testUser = new User(); + testUser.setAuthId("testAuth123"); + testUser.setProfile_image_src(null); + + when(authenticatedUserService.getAuthenticatedUser()).thenReturn(testUser); + } + + @Test + void uploadProfileImage_Success() throws Exception { + MockMultipartFile mockFile = new MockMultipartFile( + "image", + "test.png", + "image/png", + "test image content".getBytes() + ); + + mockMvc.perform(multipart("/uploadProfileImage") + .file(mockFile)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/myProfile")); + + verify(authenticatedUserService).getAuthenticatedUser(); + verify(userRepository).save(any(User.class)); + } + + // Other test methods remain the same... + private void cleanupTestFiles() throws IOException { + Path testUploadPath = Paths.get(profileUploadDir); + if (Files.exists(testUploadPath)) { + Files.walk(testUploadPath) + .map(Path::toFile) + .forEach(File::delete); + Files.deleteIfExists(testUploadPath); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/makersacademy/acebook/controller/UploadControllerTest.java b/src/test/java/com/makersacademy/acebook/controller/UploadControllerTest.java new file mode 100644 index 000000000..210b8d8e2 --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/controller/UploadControllerTest.java @@ -0,0 +1,79 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.AuthenticatedUserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +class UploadControllerTest { + + @InjectMocks + private UploadController uploadController; + + @Mock + private UserRepository userRepository; + + @Mock + private AuthenticatedUserService authenticatedUserService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + // Set the upload directory for testing - you can point to a temp folder or any folder + uploadController.profileUploadDir = "target/test-uploads/"; + } + + @Test + void testHandleImageUpload_success() throws IOException { + // Prepare mock user + User mockUser = new User(); + mockUser.setAuthId("testUser123"); + when(authenticatedUserService.getAuthenticatedUser()).thenReturn(mockUser); + + // Prepare mock MultipartFile with some content and a filename + byte[] content = "dummy image content".getBytes(); + MultipartFile mockFile = new MockMultipartFile("image", "profile.png", "image/png", content); + + // Call the method under test + String result = uploadController.handleProfileImageUpload(mockFile); + + // Verify redirect URL + assertEquals("redirect:/myProfile", result); + + // Verify userRepository.save() was called once + verify(userRepository, times(1)).save(mockUser); + + // Verify the profile_image_src was set correctly + String expectedPath = uploadController.profileUploadDir + "testUser123.png"; + assertEquals(expectedPath, mockUser.getProfile_image_src()); + + // Cleanup: delete the test uploaded file + File uploadedFile = new File(expectedPath); + if (uploadedFile.exists()) { + uploadedFile.delete(); + } + } + + @Test + void testHandleImageUpload_emptyFile() throws IOException { + MultipartFile emptyFile = new MockMultipartFile("image", new byte[0]); + + String result = uploadController.handleProfileImageUpload(emptyFile); + + assertEquals("redirect:/myProfile?error=empty", result); + + verifyNoInteractions(authenticatedUserService); + verifyNoInteractions(userRepository); + } +} diff --git a/src/test/java/com/makersacademy/acebook/controller/UploadControllerUnitTest.java b/src/test/java/com/makersacademy/acebook/controller/UploadControllerUnitTest.java new file mode 100644 index 000000000..71f556b7b --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/controller/UploadControllerUnitTest.java @@ -0,0 +1,80 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.AuthenticatedUserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class UploadControllerUnitTest { + + @Mock + private UserRepository userRepository; + + @Mock + private AuthenticatedUserService authenticatedUserService; + + @InjectMocks + private UploadController uploadController; + + private final String testUploadDir = "test-uploads"; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(uploadController, "profileUploadDir", testUploadDir); + } + + @Test + void handleImageUpload_Success() throws IOException { + // Create user without builder + User user = new User(); + user.setAuthId("auth123"); + user.setProfile_image_src(null); + + MultipartFile mockFile = new MockMultipartFile( + "image", + "test.png", + "image/png", + "test image content".getBytes() + ); + + when(authenticatedUserService.getAuthenticatedUser()).thenReturn(user); + when(userRepository.save(any(User.class))).thenReturn(user); + + String result = uploadController.handleProfileImageUpload(mockFile); + + assertEquals("redirect:/myProfile", result); + verify(authenticatedUserService).getAuthenticatedUser(); + verify(userRepository).save(user); + + cleanupTestFiles(); + } + + // Other test methods remain the same... + private void cleanupTestFiles() throws IOException { + Path testUploadPath = Paths.get(testUploadDir); + if (Files.exists(testUploadPath)) { + Files.walk(testUploadPath) + .map(Path::toFile) + .forEach(File::delete); + Files.deleteIfExists(testUploadPath); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/makersacademy/acebook/feature/FriendsTest.java b/src/test/java/com/makersacademy/acebook/feature/FriendsTest.java new file mode 100644 index 000000000..55d5e527b --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/feature/FriendsTest.java @@ -0,0 +1,62 @@ +package com.makersacademy.acebook.feature; + +import com.github.javafaker.Faker; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +//Have a look at selenium for finding text IDs +// See how to insert element IDs. +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FriendsTest { + + WebDriver driver; + Faker faker; + + @BeforeEach + public void setup() { + System.setProperty("webdriver.chrome.driver", "/usr/local/bin/chromedriver"); + driver = new ChromeDriver(); + String email = "test@test.com"; + driver.get("http://localhost:8080/"); + driver.findElement(By.name("username")).sendKeys(email); + driver.findElement(By.name("password")).sendKeys("Test123!"); + driver.findElement(By.name("action")).click(); + driver.get("http://localhost:8080/friends"); + + } + + @AfterEach + public void tearDown() { + driver.quit(); // safer than close() + } + + @Test + public void successfulTestLogInShowsTestFriends() { + String noOfFriendsText = driver.findElement(By.id("friends")).getText(); + assertEquals("Friends (6)", noOfFriendsText); + + } + + @Test + public void testLogInShowsAllFriends() { + String taylorSwift = driver.findElement(By.id("Taylor")).getText(); + String dwayneJohnson = driver.findElement(By.id("Dwayne")).getText(); + String robertDowney = driver.findElement(By.id("Robert")).getText(); + String drake = driver.findElement(By.id("Aubrey \"Drake\"")).getText(); + String emmaWatson = driver.findElement(By.id("Emma")).getText(); + String beyonce = driver.findElement(By.id("Beyoncé")).getText(); + + assertEquals("Taylor Swift", taylorSwift); + assertEquals("Dwayne Johnson", dwayneJohnson); + assertEquals("Robert Downey", robertDowney); + assertEquals("Aubrey \"Drake\" Graham", drake); + assertEquals("Emma Watson", emmaWatson); + assertEquals("Beyoncé Knowles", beyonce); + } +} diff --git a/src/test/java/com/makersacademy/acebook/feature/MyProfileTests.java b/src/test/java/com/makersacademy/acebook/feature/MyProfileTests.java new file mode 100644 index 000000000..af4fbc13e --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/feature/MyProfileTests.java @@ -0,0 +1,212 @@ +package com.makersacademy.acebook.feature; + +import com.github.javafaker.Faker; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import java.time.Duration; + +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MyProfileTests { + + WebDriver driver; + Faker faker; + WebDriverWait wait; + + @BeforeEach + public void setup() { + System.setProperty("webdriver.chrome.driver", "/usr/local/bin/chromedriver"); + driver = new ChromeDriver(); + faker = new Faker(); + wait = new WebDriverWait(driver, Duration.ofSeconds(10)); + } + + @AfterEach + public void tearDown() { + if (driver != null) { + driver.quit(); + } + } + + // Reusable helper methods + + // signs up new user with email and password + public void signUpUser() { + String email = faker.name().username() + "@email.com"; + + driver.get("http://localhost:8080/"); + driver.findElement(By.linkText("Sign up")).click(); + driver.findElement(By.name("email")).sendKeys(email); + driver.findElement(By.name("password")).sendKeys("P@55qw0rd"); + driver.findElement(By.name("action")).click(); + /* next line is optional, it adds a wait for second button ('accept') to + become clickable as this keeps throwing a stale element error when + Fran tries to run this test */ + WebElement secondActionButton = wait.until( + ExpectedConditions.elementToBeClickable(By.name("action"))); + secondActionButton.click(); + } + + @Test // myProfile should only be accessible when logged in + public void myProfileAccessibleWhenLoggedIn() { + // creates fake forename + String forename = faker.name().firstName(); + + // from sign up helper method + signUpUser(); + + // gets myProfile page + driver.get("http://localhost:8080/myProfile"); + + // asserts url is correct + assertTrue(driver.getCurrentUrl().endsWith("/myProfile")); + } + + @Test // myProfile should display logged-in user's info + public void myProfileDisplaysUserData() { + + // from sign up helper method + signUpUser(); + + // test data + String forename = faker.name().firstName(); + String surname = faker.name().lastName(); + String description = faker.lorem().sentence(); + + // gets myProfile page + driver.get("http://localhost:8080/myProfile"); + + // Forename + WebElement forenameInput = wait.until(ExpectedConditions.elementToBeClickable(By.id("forename"))); + forenameInput.clear(); + forenameInput.sendKeys(forename); + driver.findElement(By.id("update forename")).click(); + + // Surname + driver.get("http://localhost:8080/myProfile"); + WebElement surnameInput = wait.until(ExpectedConditions.elementToBeClickable(By.id("surname"))); + surnameInput.clear(); + surnameInput.sendKeys(surname); + driver.findElement(By.id("update surname")).click(); + + // go to myProfile and submit description + driver.get("http://localhost:8080/myProfile"); + WebElement descriptionInput = wait.until(ExpectedConditions.elementToBeClickable(By.id("description"))); + descriptionInput.clear(); + descriptionInput.sendKeys(description); + driver.findElement(By.id("update description")).click(); + + // Final visit to check values + driver.get("http://localhost:8080/myProfile"); + + assertEquals(forename, driver.findElement(By.id("forename")).getAttribute("value")); + assertEquals(surname, driver.findElement(By.id("surname")).getAttribute("value")); + assertEquals(description, driver.findElement(By.id("description")).getAttribute("value")); + } + + @Test // myProfile should redirect user if not logged in. + public void myProfileRedirectsIfNotLoggedIn() { + + // gets myProfile page + driver.get("http://localhost:8080/myProfile"); + + // gets current URL after navigation + String currentUrl = driver.getCurrentUrl(); + + // asserts the user was redirected (e.g., to login page) + assertTrue(currentUrl.contains("auth0.com/u/login")); + } + + @Test // myProfile should allow users to update their forename + public void myProfileUpdateForenameWorks() { + // creates fake forename + String forename = faker.name().firstName(); + + // from sign up helper method + signUpUser(); + + // gets myProfile page + driver.get("http://localhost:8080/myProfile"); + + // waits until forename field becomes clickable + WebElement forenameInput = wait.until(ExpectedConditions.elementToBeClickable(By.id("forename"))); + forenameInput.clear(); + forenameInput.sendKeys(forename); + + // waits until submit button becomes clickable + WebElement submitButton = wait.until(ExpectedConditions.elementToBeClickable(By.id("update forename"))); + submitButton.click(); + + // Wait for the forename field to update and re-fetch it + WebElement updatedForenameInput = wait.until( + ExpectedConditions.presenceOfElementLocated(By.id("forename")) + ); + String displayedForename = updatedForenameInput.getAttribute("value"); + assertEquals(forename, displayedForename); + } + + @Test // myProfile should allow users to update their surname + public void myProfileUpdateSurnameWorks() { + // creates fake surname + String surname = faker.name().lastName(); + + // from sign up helper method + signUpUser(); + + // gets myProfile page + driver.get("http://localhost:8080/myProfile"); + + // waits until surname field becomes clickable + WebElement surnameInput = wait.until(ExpectedConditions.elementToBeClickable(By.id("surname"))); + surnameInput.clear(); + surnameInput.sendKeys(surname); + + // waits until submit button becomes clickable + WebElement submitButton = wait.until(ExpectedConditions.elementToBeClickable(By.id("update surname"))); + submitButton.click(); + + // Wait for the surname field to update and re-fetch it + WebElement updatedSurnameInput = wait.until( + ExpectedConditions.presenceOfElementLocated(By.id("surname")) + ); + String displayedSurname = updatedSurnameInput.getAttribute("value"); + assertEquals(surname, displayedSurname); + } + + @Test // myProfile should allow users to update their description + public void myProfileUpdateDescriptionWorks() { + // creates fake description + String description = faker.lorem().sentence(); + + // from sign up helper method + signUpUser(); + + // gets myProfile page + driver.get("http://localhost:8080/myProfile"); + + // waits until description field becomes clickable + WebElement descriptionInput = wait.until(ExpectedConditions.elementToBeClickable(By.id("description"))); + descriptionInput.clear(); + descriptionInput.sendKeys(description); + + // waits until submit button becomes clickable + WebElement submitButton = wait.until(ExpectedConditions.elementToBeClickable(By.id("update description"))); + submitButton.click(); + + // waits for description to update + WebElement updatedDescriptionInput = wait.until( + ExpectedConditions.presenceOfElementLocated(By.id("description")) + ); + String displayedDescription = updatedDescriptionInput.getAttribute("value"); + assertEquals(description, displayedDescription); + } +} \ No newline at end of file diff --git a/src/test/java/com/makersacademy/acebook/feature/PostsControllerTest.java b/src/test/java/com/makersacademy/acebook/feature/PostsControllerTest.java new file mode 100644 index 000000000..3dda80a99 --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/feature/PostsControllerTest.java @@ -0,0 +1,155 @@ +package com.makersacademy.acebook.feature; + +import com.github.javafaker.Faker; +import org.junit.jupiter.api.*; +import org.openqa.selenium.*; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.support.ui.*; + +import java.time.Duration; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class PostsControllerTest { + + WebDriver driver; + Faker faker; + WebDriverWait wait; + + @BeforeEach + public void setup() { + System.setProperty("webdriver.chrome.driver", "/usr/local/bin/chromedriver"); + driver = new ChromeDriver(); + wait = new WebDriverWait(driver, Duration.ofSeconds(10)); + faker = new Faker(); + } + + @AfterEach + public void tearDown() { + driver.quit(); + } + + @Test // Signing in + // ADD BELOW TO EACH TEST TO ALLOW AUTHENTICATION + public void successfulSignUpAlsoLogsInUser() { + String email = faker.name().username() + "@email.com"; + driver.get("http://localhost:8080/"); + driver.findElement(By.linkText("Sign up")).click(); + driver.findElement(By.name("email")).sendKeys(email); + driver.findElement(By.name("password")).sendKeys("P@55qw0rd"); + driver.findElement(By.name("action")).click(); + WebElement secondActionButton = wait.until( + ExpectedConditions.elementToBeClickable(By.name("action"))); + secondActionButton.click(); +// String greetingText = driver.findElement(By.id("greeting")).getText(); + // AUTHENTICATION END +// assertEquals("Signed in as " + email, greetingText); + } + + + + @Test // Checking post + public void checkingPost() { + String email = faker.name().username() + "@email.com"; + // AUTHENTICATION START + driver.get("http://localhost:8080/"); + driver.findElement(By.linkText("Sign up")).click(); + driver.findElement(By.name("email")).sendKeys(email); + driver.findElement(By.name("password")).sendKeys("P@55qw0rd"); + driver.findElement(By.name("action")).click(); + WebElement secondActionButton = wait.until( + ExpectedConditions.elementToBeClickable(By.name("action"))); + secondActionButton.click(); +// String greetingText = driver.findElement(By.id("greeting")).getText(); + // POST START + driver.get("http://localhost:8080/"); + driver.findElement(By.id("post-send")).sendKeys("Hello there"); + driver.findElement(By.cssSelector("button.post-submit-button")).click(); + WebElement renderedPost = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("post-render"))); + String testPost = renderedPost.getText(); + assertEquals("Hello there", testPost); + + } + + + @Test // Checking Comment + public void checkingComment() { + String email = faker.name().username() + "@email.com"; + // AUTHENTICATION START + driver.get("http://localhost:8080/"); + driver.findElement(By.linkText("Sign up")).click(); + driver.findElement(By.name("email")).sendKeys(email); + driver.findElement(By.name("password")).sendKeys("P@55qw0rd"); + driver.findElement(By.name("action")).click(); + WebElement secondActionButton = wait.until( + ExpectedConditions.elementToBeClickable(By.name("action"))); + secondActionButton.click(); +// String greetingText = driver.findElement(By.id("greeting")).getText(); + // POST START + driver.get("http://localhost:8080/"); + driver.findElement(By.id("post-send")).sendKeys("Hello there"); + driver.findElement(By.cssSelector("button.post-submit-button")).click(); + WebElement renderedPost = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("post-render"))); + String testPost = renderedPost.getText(); + assertEquals("Hello there", testPost); + // COMMENT START + + driver.findElement(By.id("comment-send")).sendKeys("Hi again!"); + driver.findElement(By.cssSelector("button.comment-submit-button")).click(); + WebElement renderedComment = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("comment-render"))); + String testComment = renderedComment.getText(); + assertEquals("Hi again!", testComment); + } + @Test + public void checkingPostNotFoundWithoutFriend() { + // First user signs up and makes a post + String user1Email = faker.name().username() + "@email.com"; + String user1Password = "P@55qw0rd"; + + driver.get("http://localhost:8080/"); + driver.findElement(By.linkText("Sign up")).click(); + driver.findElement(By.name("email")).sendKeys(user1Email); + driver.findElement(By.name("password")).sendKeys(user1Password); + driver.findElement(By.name("action")).click(); + + WebElement secondActionButton = wait.until( + ExpectedConditions.elementToBeClickable(By.name("action")) + ); + secondActionButton.click(); + + driver.get("http://localhost:8080/"); + String postText = "Hello there"; + driver.findElement(By.id("post-send")).sendKeys(postText); + driver.findElement(By.cssSelector("button.post-submit-button")).click(); + + WebElement renderedPost = wait.until( + ExpectedConditions.visibilityOfElementLocated(By.id("post-render")) + ); + assertEquals(postText, renderedPost.getText()); + + // ===== LOGOUT ===== + driver.findElement(By.linkText("Logout")).click(); + + // ===== SECOND USER SIGNS UP & LOGS IN ===== + String user2Email = faker.name().username() + "@email.com"; + String user2Password = "P@55qw0rd"; + + driver.findElement(By.linkText("here")).click(); + driver.findElement(By.linkText("Sign up")).click(); + driver.findElement(By.name("email")).sendKeys(user2Email); + driver.findElement(By.name("password")).sendKeys(user2Password); + driver.findElement(By.name("action")).click(); + wait.until(ExpectedConditions.elementToBeClickable(By.name("action"))).click(); + + // ===== POST SHOULD NOT BE FOUND ===== + driver.get("http://localhost:8080/"); + List posts = driver.findElements(By.id("post-render")); + + boolean found = posts.stream().anyMatch(el -> el.getText().equals(postText)); + assertFalse(found, "Post by first user should NOT be visible to second user"); + + } + + +} \ No newline at end of file diff --git a/src/test/java/com/makersacademy/acebook/feature/ProfileTests.java b/src/test/java/com/makersacademy/acebook/feature/ProfileTests.java new file mode 100644 index 000000000..0d8ea8395 --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/feature/ProfileTests.java @@ -0,0 +1,112 @@ +package com.makersacademy.acebook.feature; + +import com.github.javafaker.Faker; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import java.time.Duration; + +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ProfileTests { + + WebDriver driver; + Faker faker; + WebDriverWait wait; + + @BeforeEach + public void setup() { + System.setProperty("webdriver.chrome.driver", "/usr/local/bin/chromedriver"); + driver = new ChromeDriver(); + faker = new Faker(); + wait = new WebDriverWait(driver, Duration.ofSeconds(10)); + signUpUser(); + } + + @AfterEach + public void tearDown() { + if (driver != null) { + driver.quit(); + } + } + + // Reusable helper methods + + // signs up new user with email and password + public void signUpUser() { + String email = faker.name().username() + "@email.com"; + + driver.get("http://localhost:8080/"); + driver.findElement(By.linkText("Sign up")).click(); + driver.findElement(By.name("email")).sendKeys(email); + driver.findElement(By.name("password")).sendKeys("P@55qw0rd"); + driver.findElement(By.name("action")).click(); + /* next line is optional, it adds a wait for second button ('accept') to + become clickable as this keeps throwing a stale element error when + Fran tries to run this test */ + WebElement secondActionButton = wait.until( + ExpectedConditions.elementToBeClickable(By.name("action"))); + secondActionButton.click(); + } + + @Test // profile should only be accessible when logged in + public void profilePageAccessibleWhenLoggedIn() { + + // setting user to id 2 (Taylor Swift) + String userId = "2"; + + // navigates to user's profile + driver.get("http://localhost:8080/profile/" + userId); + + // assert URL is the profile page + assertTrue(driver.getCurrentUrl().endsWith("/profile/" + userId)); + } + + @Test // profile Should display another user's info by id + public void profilePageDisplaysOtherUserData() { + + // setting user to id 2 (Taylor Swift) + String userId = "2"; + + // expected data for Taylor Swift + String expectedForename = "Taylor"; + String expectedSurname = "Swift"; + String expectedDescription = "writing songs in lowercase & living eras one heartbreak at a time."; + + // navigates to user's profile + driver.get("http://localhost:8080/profile/" + userId); + + // waits for fields to become visible + WebElement forenameSpan = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("forename"))); + WebElement surnameSpan = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("surname"))); + WebElement descriptionSpan = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("description"))); + + // assert the displayed user info matches expected data for Taylor Swift + assertEquals(expectedForename, forenameSpan.getText()); + assertEquals(expectedSurname, surnameSpan.getText()); + assertEquals(expectedDescription, descriptionSpan.getText()); + } + + @Test // profile should not display invalid user + public void profilePageGives404ForInvalidUser() { + + // setting invalid user id + String invalidUserId = "999999"; + + // getting profile page for invalid id + driver.get("http://localhost:8080/profile/" + invalidUserId); + + // checking if page displays 404 message or error text + String pageSource = driver.getPageSource(); + assertTrue("Page should show 404 or error message", + pageSource.contains("404") || pageSource.contains("User not found") || pageSource.contains("error")); + } +} \ No newline at end of file diff --git a/src/test/java/com/makersacademy/acebook/feature/SignUpTest.java b/src/test/java/com/makersacademy/acebook/feature/SignUpTest.java index dcb1416bb..352b92036 100644 --- a/src/test/java/com/makersacademy/acebook/feature/SignUpTest.java +++ b/src/test/java/com/makersacademy/acebook/feature/SignUpTest.java @@ -1,29 +1,39 @@ package com.makersacademy.acebook.feature; import com.github.javafaker.Faker; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertEquals; public class SignUpTest { WebDriver driver; Faker faker; + WebDriverWait wait; - @Before + @BeforeEach public void setup() { System.setProperty("webdriver.chrome.driver", "/usr/local/bin/chromedriver"); driver = new ChromeDriver(); faker = new Faker(); + wait = new WebDriverWait(driver, Duration.ofSeconds(10)); } - @After + @AfterEach public void tearDown() { - driver.close(); + if (driver != null) { + driver.quit(); + } } @Test @@ -35,8 +45,11 @@ public void successfulSignUpAlsoLogsInUser() { driver.findElement(By.name("email")).sendKeys(email); driver.findElement(By.name("password")).sendKeys("P@55qw0rd"); driver.findElement(By.name("action")).click(); - driver.findElement(By.name("action")).click(); + WebElement secondActionButton = wait.until( + ExpectedConditions.elementToBeClickable(By.name("action"))); + secondActionButton.click(); String greetingText = driver.findElement(By.id("greeting")).getText(); - Assert.assertEquals("Signed in as " + email, greetingText); + + assertEquals("Signed in as " + email, greetingText); } } diff --git a/src/test/java/com/makersacademy/acebook/model/PostTest.java b/src/test/java/com/makersacademy/acebook/model/PostTest.java index 926d9b1bf..e4900a096 100644 --- a/src/test/java/com/makersacademy/acebook/model/PostTest.java +++ b/src/test/java/com/makersacademy/acebook/model/PostTest.java @@ -7,7 +7,7 @@ public class PostTest { - private Post post = new Post("hello"); + private Post post = new Post("hello", "NULL", null, "NULL", null, null, null); @Test public void postHasContent() { diff --git a/src/test/java/com/makersacademy/acebook/model/UserTest.java b/src/test/java/com/makersacademy/acebook/model/UserTest.java new file mode 100644 index 000000000..f56539abb --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/model/UserTest.java @@ -0,0 +1,22 @@ +package com.makersacademy.acebook.model; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; + +import org.junit.jupiter.api.Test; + +public class UserTest { + + private User karen = new User("Karen", true); + private User zehad = new User("Zehad", true); + + @Test + public void checkWhoIsFriends() { + // Hashset friends table is not bidirectional, + // Both friends must be added each other + karen.getFriends().add(zehad); + zehad.getFriends().add(karen); + assertThat(karen.getFriends(), hasItem(zehad)); + assertThat(zehad.getFriends(), hasItem(karen)); + } +} diff --git "a/uploads/profile/\nexport UPLOAD_PROFILE_PATH=/Users/francesparsons/Projects/Acebook/Acebook_Volcano_Project/uploads/posts/google-oauth2_115795854088591160458.jpg" "b/uploads/profile/\nexport UPLOAD_PROFILE_PATH=/Users/francesparsons/Projects/Acebook/Acebook_Volcano_Project/uploads/posts/google-oauth2_115795854088591160458.jpg" new file mode 100644 index 000000000..2515d7371 Binary files /dev/null and "b/uploads/profile/\nexport UPLOAD_PROFILE_PATH=/Users/francesparsons/Projects/Acebook/Acebook_Volcano_Project/uploads/posts/google-oauth2_115795854088591160458.jpg" differ diff --git a/uploads/profile/auth0_683868cbb2fd0f68bc0670d5.jpeg b/uploads/profile/auth0_683868cbb2fd0f68bc0670d5.jpeg new file mode 100644 index 000000000..b86c4733e Binary files /dev/null and b/uploads/profile/auth0_683868cbb2fd0f68bc0670d5.jpeg differ diff --git a/uploads/profile/beyonce.jpg b/uploads/profile/beyonce.jpg new file mode 100644 index 000000000..9de09c891 Binary files /dev/null and b/uploads/profile/beyonce.jpg differ diff --git a/uploads/profile/drake.jpg b/uploads/profile/drake.jpg new file mode 100644 index 000000000..06ea9ca7d Binary files /dev/null and b/uploads/profile/drake.jpg differ diff --git a/uploads/profile/emmawatson.jpg b/uploads/profile/emmawatson.jpg new file mode 100644 index 000000000..5fcae95cf Binary files /dev/null and b/uploads/profile/emmawatson.jpg differ diff --git a/uploads/profile/rihanna.jpg b/uploads/profile/rihanna.jpg new file mode 100644 index 000000000..e6c59322d Binary files /dev/null and b/uploads/profile/rihanna.jpg differ diff --git a/uploads/profile/robertdj.jpg b/uploads/profile/robertdj.jpg new file mode 100644 index 000000000..7c2bc8ca9 Binary files /dev/null and b/uploads/profile/robertdj.jpg differ diff --git a/uploads/profile/taylor_swift.jpg b/uploads/profile/taylor_swift.jpg new file mode 100644 index 000000000..3d9dad684 Binary files /dev/null and b/uploads/profile/taylor_swift.jpg differ diff --git a/uploads/profile/therock.jpg b/uploads/profile/therock.jpg new file mode 100644 index 000000000..046746f52 Binary files /dev/null and b/uploads/profile/therock.jpg differ diff --git a/uploads/profile/zendaya.jpg b/uploads/profile/zendaya.jpg new file mode 100644 index 000000000..ae287176f Binary files /dev/null and b/uploads/profile/zendaya.jpg differ