diff --git a/.gitignore b/.gitignore index 20f3af3..3f3f28c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ build .vscode .idea .DS_Store -app/.env \ No newline at end of file +app/.env +.env \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5c8d388..293506a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -84,9 +84,11 @@ dependencies { runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' // https://mvnrepository.com/artifact/io.github.cdimascio/java-dotenv implementation group: 'io.github.cdimascio', name: 'java-dotenv', version: '5.2.2' - + implementation("com.stripe:stripe-java:29.2.0") // Use JUnit test framework. testImplementation libs.junit + testImplementation("org.mockito:mockito-core:5.17.0") + testImplementation 'org.springframework.boot:spring-boot-starter-test:3.3.3' } diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/config/JwtAuthenticationFilter.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/config/JwtAuthenticationFilter.java index 75eff50..5ee5a94 100644 --- a/app/src/main/java/fr/parisnanterre/greentrip/backend/config/JwtAuthenticationFilter.java +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/config/JwtAuthenticationFilter.java @@ -13,6 +13,7 @@ import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; +import io.jsonwebtoken.ExpiredJwtException; import java.io.IOException; @@ -41,6 +42,8 @@ protected void doFilterInternal( if (authHeader == null || !authHeader.startsWith("Bearer ")) { System.out.println("No Authorization header or invalid format."); // Debug log + System.out.println("➡️ Requête vers : " + request.getRequestURI()); + System.out.println("🔐 Authorization header : " + request.getHeader("Authorization")); filterChain.doFilter(request, response); return; } @@ -53,7 +56,13 @@ protected void doFilterInternal( } System.out.println("Extracted JWT: " + jwt); // Debug log - userEmail = jwtService.extractUserName(jwt); + try { + userEmail = jwtService.extractUserName(jwt); + } catch (ExpiredJwtException e) { + System.err.println("⛔ JWT expiré : " + e.getMessage()); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } System.out.println("Extracted email from JWT: " + userEmail); // Debug log if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/config/SecurityConfiguration.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/config/SecurityConfiguration.java index 945b5de..04a757e 100644 --- a/app/src/main/java/fr/parisnanterre/greentrip/backend/config/SecurityConfiguration.java +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/config/SecurityConfiguration.java @@ -47,7 +47,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/api/v1/auth/logout", "/api/trips/**", "/api/waypoints/**", - "/api/v1/news/views/export" + "/api/v1/news/views/export", + "/api/v1/payment/**", + "/api/v1/cart/**" ).authenticated() .anyRequest().authenticated() ) diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/CartController.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/CartController.java new file mode 100644 index 0000000..9f25910 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/CartController.java @@ -0,0 +1,39 @@ +package fr.parisnanterre.greentrip.backend.controller; + +import fr.parisnanterre.greentrip.backend.entity.CartItem; +import fr.parisnanterre.greentrip.backend.entity.User; +import fr.parisnanterre.greentrip.backend.service.CartService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/cart") +@RequiredArgsConstructor +public class CartController { + + private final CartService cartService; + + @GetMapping + public List getCart(@AuthenticationPrincipal User user) { + return cartService.getCart(user); + } + + @PostMapping("/{productId}") + public void addToCart(@AuthenticationPrincipal User user, @PathVariable Long productId) { + cartService.addToCart(user, productId); + } + + @DeleteMapping("/{productId}") + public void removeFromCart(@AuthenticationPrincipal User user, @PathVariable Long productId) { + cartService.removeFromCart(user, productId); + } + + @DeleteMapping("/clear") + public void clearCart(@AuthenticationPrincipal User user) { + cartService.clearCart(user); + } + +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/OrderController.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/OrderController.java new file mode 100644 index 0000000..cf4e7ca --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/OrderController.java @@ -0,0 +1,28 @@ +package fr.parisnanterre.greentrip.backend.controller; + +import fr.parisnanterre.greentrip.backend.entity.Order; +import fr.parisnanterre.greentrip.backend.entity.User; +import fr.parisnanterre.greentrip.backend.service.OrderService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/orders") +@RequiredArgsConstructor +public class OrderController { + + private final OrderService orderService; + + @PostMapping + public void createOrder(@AuthenticationPrincipal User user) { + orderService.createOrderFromCart(user); + } + + @GetMapping + public List getOrders(@AuthenticationPrincipal User user) { + return orderService.getOrders(user); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/PaymentController.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/PaymentController.java new file mode 100644 index 0000000..8012ee8 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/PaymentController.java @@ -0,0 +1,33 @@ +package fr.parisnanterre.greentrip.backend.controller; + +import com.stripe.model.PaymentIntent; +import fr.parisnanterre.greentrip.backend.dto.PaymentRequest; +import fr.parisnanterre.greentrip.backend.service.PaymentService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/payment") +@RequiredArgsConstructor +public class PaymentController { + + private final PaymentService paymentService; + + @PostMapping + public ResponseEntity> pay( + @RequestBody PaymentRequest req, + @AuthenticationPrincipal UserDetails user + ) throws Exception { + PaymentIntent intent = paymentService.handlePayment(req, user.getUsername()); + + return ResponseEntity.ok(Map.of("clientSecret", intent.getClientSecret())); + } +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/ProductController.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/ProductController.java new file mode 100644 index 0000000..af877a0 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/ProductController.java @@ -0,0 +1,23 @@ +package fr.parisnanterre.greentrip.backend.controller; + +import fr.parisnanterre.greentrip.backend.entity.Product; +import fr.parisnanterre.greentrip.backend.repository.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/products") +@RequiredArgsConstructor +public class ProductController { + + private final ProductRepository productRepo; + + @GetMapping + public List getAllProducts() { + return productRepo.findAll(); + } +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/dto/PaymentRequest.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/dto/PaymentRequest.java new file mode 100644 index 0000000..03a1331 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/dto/PaymentRequest.java @@ -0,0 +1,13 @@ +package fr.parisnanterre.greentrip.backend.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PaymentRequest { + + private Double amount; +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/CartItem.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/CartItem.java new file mode 100644 index 0000000..2897625 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/CartItem.java @@ -0,0 +1,27 @@ +package fr.parisnanterre.greentrip.backend.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Table(name = "cart") +public class CartItem { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + private User user; + + @ManyToOne + private Product product; + + private int quantity; +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/Order.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/Order.java new file mode 100644 index 0000000..901c498 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/Order.java @@ -0,0 +1,28 @@ +package fr.parisnanterre.greentrip.backend.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "`order`") // `order` est un mot réservé en SQL +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Order { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + private User user; + + @ManyToOne(optional = false) + private Product product; + + private int quantity; + + private LocalDateTime orderDate; +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/PaymentHistory.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/PaymentHistory.java new file mode 100644 index 0000000..16a43a6 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/PaymentHistory.java @@ -0,0 +1,33 @@ +package fr.parisnanterre.greentrip.backend.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Table(name = "payment_history") +public class PaymentHistory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + private Product product; + + @ManyToOne + private User user; + + private Double amount; + private String status; + private String paymentIntentId; + private LocalDateTime createdAt; +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/Product.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/Product.java new file mode 100644 index 0000000..182eaed --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/Product.java @@ -0,0 +1,35 @@ +package fr.parisnanterre.greentrip.backend.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Table(name = "product") +public class Product { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + private String description; + private Double price; + @Column(nullable = false) + private int stock; + private String imageUrl; + + public Product(String name, String description, double price, String imageUrl, int stock) { + this.name = name; + this.description = description; + this.price = price; + this.imageUrl = imageUrl; + this.stock = stock; + } +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/CartItemRepository.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/CartItemRepository.java new file mode 100644 index 0000000..8d945bc --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/CartItemRepository.java @@ -0,0 +1,20 @@ +package fr.parisnanterre.greentrip.backend.repository; + +import fr.parisnanterre.greentrip.backend.entity.CartItem; +import fr.parisnanterre.greentrip.backend.entity.Product; +import fr.parisnanterre.greentrip.backend.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface CartItemRepository extends JpaRepository { + + List findByUser(User user); + + Optional findByUserAndProduct(User user, Product product); + + void deleteByUserAndProduct(User user, Product product); + + void deleteAllByUser(User user); +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/OrderRepository.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/OrderRepository.java new file mode 100644 index 0000000..934560f --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/OrderRepository.java @@ -0,0 +1,12 @@ +package fr.parisnanterre.greentrip.backend.repository; + +import fr.parisnanterre.greentrip.backend.entity.Order; +import fr.parisnanterre.greentrip.backend.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface OrderRepository extends JpaRepository { + + List findByUser(User user); +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/PaymentHistoryRepository.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/PaymentHistoryRepository.java new file mode 100644 index 0000000..54841c4 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/PaymentHistoryRepository.java @@ -0,0 +1,7 @@ +package fr.parisnanterre.greentrip.backend.repository; + +import fr.parisnanterre.greentrip.backend.entity.PaymentHistory; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PaymentHistoryRepository extends JpaRepository { +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/ProductRepository.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/ProductRepository.java new file mode 100644 index 0000000..caf7b68 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/ProductRepository.java @@ -0,0 +1,7 @@ +package fr.parisnanterre.greentrip.backend.repository; + +import fr.parisnanterre.greentrip.backend.entity.Product; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProductRepository extends JpaRepository { +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/service/CartService.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/CartService.java new file mode 100644 index 0000000..17112fc --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/CartService.java @@ -0,0 +1,55 @@ +package fr.parisnanterre.greentrip.backend.service; + +import fr.parisnanterre.greentrip.backend.entity.CartItem; +import fr.parisnanterre.greentrip.backend.entity.Product; +import fr.parisnanterre.greentrip.backend.entity.User; +import fr.parisnanterre.greentrip.backend.repository.CartItemRepository; +import fr.parisnanterre.greentrip.backend.repository.ProductRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class CartService { + + private final CartItemRepository cartRepo; + private final ProductRepository productRepo; + + public List getCart(User user) { + return cartRepo.findByUser(user); + } + + public void addToCart(User user, Long productId) { + Product product = productRepo.findById(productId) + .orElseThrow(() -> new RuntimeException("Produit introuvable")); + + cartRepo.findByUserAndProduct(user, product).ifPresentOrElse( + item -> { + if (item.getQuantity() < product.getStock()) { + item.setQuantity(item.getQuantity() + 1); + cartRepo.save(item); + } + }, + () -> cartRepo.save(CartItem.builder() + .user(user) + .product(product) + .quantity(1) + .build()) + ); + } + + public void removeFromCart(User user, Long productId) { + Product product = productRepo.findById(productId) + .orElseThrow(() -> new RuntimeException("Produit introuvable")); + + cartRepo.deleteByUserAndProduct(user, product); + } + + public void clearCart(User user) { + cartRepo.deleteAllByUser(user); + } +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/service/OrderService.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/OrderService.java new file mode 100644 index 0000000..6884748 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/OrderService.java @@ -0,0 +1,42 @@ +package fr.parisnanterre.greentrip.backend.service; + +import fr.parisnanterre.greentrip.backend.entity.*; +import fr.parisnanterre.greentrip.backend.repository.OrderRepository; +import fr.parisnanterre.greentrip.backend.repository.ProductRepository; +import fr.parisnanterre.greentrip.backend.repository.CartItemRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class OrderService { + + private final OrderRepository orderRepo; + private final CartItemRepository cartRepo; + private final ProductRepository productRepo; + + @Transactional + public void createOrderFromCart(User user) { + List cartItems = cartRepo.findByUser(user); + + for (CartItem item : cartItems) { + Order order = Order.builder() + .user(user) + .product(item.getProduct()) + .quantity(item.getQuantity()) + .orderDate(LocalDateTime.now()) + .build(); + orderRepo.save(order); + } + + cartRepo.deleteAll(cartItems); // vider le panier après commande + } + + public List getOrders(User user) { + return orderRepo.findByUser(user); + } +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/service/PaymentService.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/PaymentService.java new file mode 100644 index 0000000..4ae0121 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/PaymentService.java @@ -0,0 +1,47 @@ +package fr.parisnanterre.greentrip.backend.service; + +import com.stripe.model.PaymentIntent; +import fr.parisnanterre.greentrip.backend.dto.PaymentRequest; +import fr.parisnanterre.greentrip.backend.entity.PaymentHistory; +import fr.parisnanterre.greentrip.backend.entity.Product; +import fr.parisnanterre.greentrip.backend.entity.User; +import fr.parisnanterre.greentrip.backend.repository.PaymentHistoryRepository; +import fr.parisnanterre.greentrip.backend.repository.ProductRepository; +import fr.parisnanterre.greentrip.backend.repository.UserRepository; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +@AllArgsConstructor +public class PaymentService { + + private final StripeService stripeService; + private final ProductRepository productRepo; + private final PaymentHistoryRepository historyRepo; + private final UserRepository userRepo; + + public PaymentIntent handlePayment(PaymentRequest req, String userEmail) throws Exception { + + User user = userRepo.findByEmail(userEmail); + if (user == null) { + throw new RuntimeException("Utilisateur non trouvé"); + } + + PaymentIntent intent = stripeService.createPaymentIntent(req.getAmount()); + + // Historiser + PaymentHistory history = PaymentHistory.builder() + .user(user) + .createdAt(LocalDateTime.now()) + .amount(req.getAmount()) + .status("CREATED") + .paymentIntentId(intent.getId()) + .build(); + + historyRepo.save(history); + + return intent; + } +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/service/StripeService.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/StripeService.java new file mode 100644 index 0000000..76236a1 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/StripeService.java @@ -0,0 +1,36 @@ +package fr.parisnanterre.greentrip.backend.service; + +import com.stripe.Stripe; +import com.stripe.exception.StripeException; +import com.stripe.model.PaymentIntent; +import com.stripe.param.PaymentIntentCreateParams; +import io.github.cdimascio.dotenv.Dotenv; +import jakarta.annotation.PostConstruct; +import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class StripeService { + + @Value("${stripe.secret.key}") + private String secretKey; + + @PostConstruct + public void init() { + if (secretKey == null || secretKey.isEmpty()) { + throw new RuntimeException("❌ Clé Stripe absente dans les variables d’environnement !"); + } + Stripe.apiKey = secretKey; + System.out.println("✅ Clé Stripe chargée depuis les variables d’environnement !"); + } + + public PaymentIntent createPaymentIntent(Double amount) throws StripeException { + PaymentIntentCreateParams params = PaymentIntentCreateParams.builder() + .setAmount((long) (amount * 100)) // convertir en centimes + .setCurrency("eur") + .build(); + + return PaymentIntent.create(params); + } +} diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml index 080f6df..bd88093 100644 --- a/app/src/main/resources/application.yml +++ b/app/src/main/resources/application.yml @@ -29,3 +29,6 @@ springdoc: swagger-ui: path: /swagger-ui.html +stripe: + secret: + key: ${SECRET_STRIPE_KEY} \ No newline at end of file diff --git a/app/src/test/java/fr/parisnanterre/greentrip/backend/controller/CartControllerTest.java b/app/src/test/java/fr/parisnanterre/greentrip/backend/controller/CartControllerTest.java new file mode 100644 index 0000000..05573c5 --- /dev/null +++ b/app/src/test/java/fr/parisnanterre/greentrip/backend/controller/CartControllerTest.java @@ -0,0 +1,60 @@ +package fr.parisnanterre.greentrip.backend.controller; + +import fr.parisnanterre.greentrip.backend.controller.CartController; +import fr.parisnanterre.greentrip.backend.entity.CartItem; +import fr.parisnanterre.greentrip.backend.entity.User; +import fr.parisnanterre.greentrip.backend.service.CartService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.*; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +public class CartControllerTest { + + @Mock + private CartService cartService; + + @InjectMocks + private CartController cartController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testGetCart() { + User user = new User(); + CartItem item = new CartItem(); + when(cartService.getCart(user)).thenReturn(Collections.singletonList(item)); + + List cart = cartController.getCart(user); + assertEquals(1, cart.size()); + } + + @Test + public void testAddToCart() { + User user = new User(); + cartController.addToCart(user, 1L); + verify(cartService).addToCart(user, 1L); + } + + @Test + public void testRemoveFromCart() { + User user = new User(); + cartController.removeFromCart(user, 1L); + verify(cartService).removeFromCart(user, 1L); + } + + @Test + public void testClearCart() { + User user = new User(); + cartController.clearCart(user); + verify(cartService).clearCart(user); + } +} diff --git a/app/src/test/java/fr/parisnanterre/greentrip/backend/controller/OrderControllerTest.java b/app/src/test/java/fr/parisnanterre/greentrip/backend/controller/OrderControllerTest.java new file mode 100644 index 0000000..5a36639 --- /dev/null +++ b/app/src/test/java/fr/parisnanterre/greentrip/backend/controller/OrderControllerTest.java @@ -0,0 +1,46 @@ +package fr.parisnanterre.greentrip.backend.controller; + +import fr.parisnanterre.greentrip.backend.controller.OrderController; +import fr.parisnanterre.greentrip.backend.entity.Order; +import fr.parisnanterre.greentrip.backend.entity.User; +import fr.parisnanterre.greentrip.backend.service.OrderService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.*; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +public class OrderControllerTest { + + @Mock + private OrderService orderService; + + @InjectMocks + private OrderController orderController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testGetOrders() { + User user = new User(); + Order order = new Order(); + when(orderService.getOrders(user)).thenReturn(Collections.singletonList(order)); + + List orders = orderController.getOrders(user); + assertEquals(1, orders.size()); + } + + @Test + public void testCreateOrder() { + User user = new User(); + orderController.createOrder(user); + verify(orderService).createOrderFromCart(user); + } +} diff --git a/app/src/test/java/fr/parisnanterre/greentrip/backend/controller/PaymentControllerTest.java b/app/src/test/java/fr/parisnanterre/greentrip/backend/controller/PaymentControllerTest.java new file mode 100644 index 0000000..e4f05c5 --- /dev/null +++ b/app/src/test/java/fr/parisnanterre/greentrip/backend/controller/PaymentControllerTest.java @@ -0,0 +1,50 @@ +package fr.parisnanterre.greentrip.backend.controller; + +import com.stripe.model.PaymentIntent; +import fr.parisnanterre.greentrip.backend.controller.PaymentController; +import fr.parisnanterre.greentrip.backend.dto.PaymentRequest; +import fr.parisnanterre.greentrip.backend.service.PaymentService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.http.ResponseEntity; + +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +public class PaymentControllerTest { + + @Mock + private PaymentService paymentService; + + @InjectMocks + private PaymentController paymentController; + + @Mock + private UserDetails userDetails; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testPay() throws Exception { + PaymentRequest req = new PaymentRequest(); + req.setAmount(42.0); + PaymentIntent mockIntent = mock(PaymentIntent.class); + when(mockIntent.getClientSecret()).thenReturn("test_secret"); + + when(paymentService.handlePayment(req, "test@example.com")).thenReturn(mockIntent); + when(userDetails.getUsername()).thenReturn("test@example.com"); + + ResponseEntity> response = paymentController.pay(req, userDetails); + + assertEquals("test_secret", response.getBody().get("clientSecret")); + } +} diff --git a/app/src/test/java/fr/parisnanterre/greentrip/backend/controller/ProductControllerTest.java b/app/src/test/java/fr/parisnanterre/greentrip/backend/controller/ProductControllerTest.java new file mode 100644 index 0000000..a2e965d --- /dev/null +++ b/app/src/test/java/fr/parisnanterre/greentrip/backend/controller/ProductControllerTest.java @@ -0,0 +1,45 @@ +package fr.parisnanterre.greentrip.backend.controller; + +import fr.parisnanterre.greentrip.backend.controller.ProductController; +import fr.parisnanterre.greentrip.backend.entity.Product; +import fr.parisnanterre.greentrip.backend.repository.ProductRepository; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +public class ProductControllerTest { + + @Mock + private ProductRepository productRepo; + + @InjectMocks + private ProductController productController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testGetAllProducts() { + Product p1 = new Product(); + p1.setName("Bracelet"); + Product p2 = new Product(); + p2.setName("Bague"); + + when(productRepo.findAll()).thenReturn(Arrays.asList(p1, p2)); + + List result = productController.getAllProducts(); + + assertEquals(2, result.size()); + assertEquals("Bracelet", result.get(0).getName()); + } +} diff --git a/app/src/test/java/fr/parisnanterre/greentrip/backend/service/CartServiceTest.java b/app/src/test/java/fr/parisnanterre/greentrip/backend/service/CartServiceTest.java new file mode 100644 index 0000000..63d4d93 --- /dev/null +++ b/app/src/test/java/fr/parisnanterre/greentrip/backend/service/CartServiceTest.java @@ -0,0 +1,99 @@ +package fr.parisnanterre.greentrip.backend.service; + +import fr.parisnanterre.greentrip.backend.entity.CartItem; +import fr.parisnanterre.greentrip.backend.entity.Product; +import fr.parisnanterre.greentrip.backend.entity.User; +import fr.parisnanterre.greentrip.backend.repository.CartItemRepository; +import fr.parisnanterre.greentrip.backend.repository.ProductRepository; +import fr.parisnanterre.greentrip.backend.service.CartService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; + +public class CartServiceTest { + + @Mock + private CartItemRepository cartRepo; + + @Mock + private ProductRepository productRepo; + + @InjectMocks + private CartService cartService; + + private User user; + private Product product; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + user = User.builder().id(1L).email("test@example.com").build(); + product = Product.builder().id(10L).name("Produit A").stock(5).build(); + } + + @Test + public void testGetCart() { + CartItem item = CartItem.builder().user(user).product(product).quantity(2).build(); + when(cartRepo.findByUser(user)).thenReturn(Arrays.asList(item)); + + List result = cartService.getCart(user); + + assertEquals(1, result.size()); + assertEquals(item, result.get(0)); + } + + @Test + public void testAddToCart_WhenItemAlreadyExistsAndStockAvailable() { + CartItem existingItem = CartItem.builder().user(user).product(product).quantity(2).build(); + + when(productRepo.findById(product.getId())).thenReturn(Optional.of(product)); + when(cartRepo.findByUserAndProduct(user, product)).thenReturn(Optional.of(existingItem)); + + cartService.addToCart(user, product.getId()); + + verify(cartRepo).save(existingItem); + assertEquals(3, existingItem.getQuantity()); + } + + @Test + public void testAddToCart_WhenItemDoesNotExist() { + when(productRepo.findById(product.getId())).thenReturn(Optional.of(product)); + when(cartRepo.findByUserAndProduct(user, product)).thenReturn(Optional.empty()); + + cartService.addToCart(user, product.getId()); + + verify(cartRepo).save(argThat(item + -> item.getUser().equals(user) + && item.getProduct().equals(product) + && item.getQuantity() == 1 + )); + } + + @Test(expected = RuntimeException.class) + public void testAddToCart_ProductNotFound() { + when(productRepo.findById(product.getId())).thenReturn(Optional.empty()); + cartService.addToCart(user, product.getId()); + } + + @Test + public void testRemoveFromCart() { + when(productRepo.findById(product.getId())).thenReturn(Optional.of(product)); + + cartService.removeFromCart(user, product.getId()); + + verify(cartRepo).deleteByUserAndProduct(user, product); + } + + @Test + public void testClearCart() { + cartService.clearCart(user); + verify(cartRepo).deleteAllByUser(user); + } +} diff --git a/app/src/test/java/fr/parisnanterre/greentrip/backend/service/OrderServiceTest.java b/app/src/test/java/fr/parisnanterre/greentrip/backend/service/OrderServiceTest.java new file mode 100644 index 0000000..18e9bac --- /dev/null +++ b/app/src/test/java/fr/parisnanterre/greentrip/backend/service/OrderServiceTest.java @@ -0,0 +1,87 @@ +package fr.parisnanterre.greentrip.backend.service; + +import fr.parisnanterre.greentrip.backend.entity.*; +import fr.parisnanterre.greentrip.backend.repository.*; +import fr.parisnanterre.greentrip.backend.service.OrderService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.*; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; + +public class OrderServiceTest { + + @Mock + private OrderRepository orderRepo; + + @Mock + private CartItemRepository cartRepo; + + @Mock + private ProductRepository productRepo; + + @InjectMocks + private OrderService orderService; + + private User user; + private Product product; + private CartItem cartItem; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + user = new User(); + user.setId(1L); + user.setEmail("test@mail.com"); + + product = new Product(); + product.setId(10L); + product.setName("Produit Test"); + product.setPrice(20.0); + + cartItem = new CartItem(); + cartItem.setId(100L); + cartItem.setUser(user); + cartItem.setProduct(product); + cartItem.setQuantity(3); + } + + @Test + public void testGetOrders_returnsOrdersForUser() { + Order order = Order.builder().user(user).product(product).quantity(2).orderDate(LocalDateTime.now()).build(); + when(orderRepo.findByUser(user)).thenReturn(Arrays.asList(order)); + + List result = orderService.getOrders(user); + + verify(orderRepo, times(1)).findByUser(user); + assertEquals(1, result.size()); + assertEquals(user, result.get(0).getUser()); + } + + @Test + public void testCreateOrderFromCart_shouldSaveOrdersAndClearCart() { + List cartItems = Arrays.asList(cartItem); + when(cartRepo.findByUser(user)).thenReturn(cartItems); + + orderService.createOrderFromCart(user); + + // ✅ cartRepo.findByUser + verify(cartRepo, times(1)).findByUser(user); + + // ✅ orderRepo.save called once + verify(orderRepo, times(1)).save(argThat(order + -> order.getUser().equals(user) + && order.getProduct().equals(product) + && order.getQuantity() == 3 + )); + + // ✅ cartRepo.deleteAll called + verify(cartRepo, times(1)).deleteAll(cartItems); + } +} diff --git a/app/src/test/java/fr/parisnanterre/greentrip/backend/service/PaymentServiceTest.java b/app/src/test/java/fr/parisnanterre/greentrip/backend/service/PaymentServiceTest.java new file mode 100644 index 0000000..4ad40b6 --- /dev/null +++ b/app/src/test/java/fr/parisnanterre/greentrip/backend/service/PaymentServiceTest.java @@ -0,0 +1,91 @@ +package fr.parisnanterre.greentrip.backend.service; + +import com.stripe.model.PaymentIntent; +import fr.parisnanterre.greentrip.backend.dto.PaymentRequest; +import fr.parisnanterre.greentrip.backend.entity.User; +import fr.parisnanterre.greentrip.backend.repository.PaymentHistoryRepository; +import fr.parisnanterre.greentrip.backend.repository.ProductRepository; +import fr.parisnanterre.greentrip.backend.repository.UserRepository; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; + +public class PaymentServiceTest { + + private StripeService stripeService; + private ProductRepository productRepo; + private PaymentHistoryRepository historyRepo; + private UserRepository userRepo; + + private PaymentService paymentService; + + @Before + public void setUp() { + stripeService = mock(StripeService.class); + productRepo = mock(ProductRepository.class); + historyRepo = mock(PaymentHistoryRepository.class); + userRepo = mock(UserRepository.class); + + paymentService = new PaymentService(stripeService, productRepo, historyRepo, userRepo); + } + + @Test + public void testHandlePayment_success() throws Exception { + // Arrange + String email = "user@example.com"; + double amount = 49.99; + + User mockUser = new User(); + mockUser.setId(1L); + mockUser.setEmail(email); + + PaymentIntent mockIntent = new PaymentIntent(); + mockIntent.setId("pi_123456"); + + when(userRepo.findByEmail(email)).thenReturn(mockUser); + when(stripeService.createPaymentIntent(amount)).thenReturn(mockIntent); + + PaymentRequest req = new PaymentRequest(); + req.setAmount(amount); + + // Act + PaymentIntent result = paymentService.handlePayment(req, email); + + // Assert + assertEquals("pi_123456", result.getId()); + verify(userRepo).findByEmail(email); + verify(stripeService).createPaymentIntent(amount); + + // Capture the PaymentHistory saved + ArgumentCaptor captor + = ArgumentCaptor.forClass(fr.parisnanterre.greentrip.backend.entity.PaymentHistory.class); + verify(historyRepo).save(captor.capture()); + + fr.parisnanterre.greentrip.backend.entity.PaymentHistory savedHistory = captor.getValue(); + assertEquals(mockUser, savedHistory.getUser()); + assertEquals("CREATED", savedHistory.getStatus()); + assertEquals("pi_123456", savedHistory.getPaymentIntentId()); + assertEquals(amount, savedHistory.getAmount(), 0.001); + assertNotNull(savedHistory.getCreatedAt()); + } + + @Test(expected = RuntimeException.class) + public void testHandlePayment_userNotFound() throws Exception { + // Arrange + String email = "notfound@example.com"; + PaymentRequest req = new PaymentRequest(); + req.setAmount(49.99); + + when(userRepo.findByEmail(email)).thenReturn(null); // Simule utilisateur non trouvé + + // Act + paymentService.handlePayment(req, email); + + // Assert + // Exception attendue → aucune autre vérification nécessaire ici + } + +} diff --git a/app/src/test/java/fr/parisnanterre/greentrip/backend/service/UserServiceTest.java b/app/src/test/java/fr/parisnanterre/greentrip/backend/service/UserServiceTest.java new file mode 100644 index 0000000..451204c --- /dev/null +++ b/app/src/test/java/fr/parisnanterre/greentrip/backend/service/UserServiceTest.java @@ -0,0 +1,68 @@ +package fr.parisnanterre.greentrip.backend.service; + +import fr.parisnanterre.greentrip.backend.entity.Role; +import fr.parisnanterre.greentrip.backend.entity.User; +import fr.parisnanterre.greentrip.backend.repository.UserRepository; +import fr.parisnanterre.greentrip.backend.service.UserService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.*; + +import java.util.Arrays; +import java.util.Optional; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +public class UserServiceTest { + + @Mock + private UserRepository userRepository; + + @InjectMocks + private UserService userService; + + private User user; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + user = User.builder() + .id(1L) + .email("admin@example.com") + .role(Role.valueOf("ADMIN")) + .build(); + } + + @Test + public void testFindByEmail() { + when(userRepository.findByEmail("admin@example.com")).thenReturn(user); + + User result = userService.findByEmail("admin@example.com"); + + assertNotNull(result); + assertEquals("admin@example.com", result.getEmail()); + verify(userRepository).findByEmail("admin@example.com"); + } + + @Test + public void testFindUserById_Found() { + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + + User result = userService.findUserById(1L); + + assertNotNull(result); + assertEquals(Long.valueOf(1L), result.getId()); + verify(userRepository).findById(1L); + } + + @Test + public void testFindUserById_NotFound() { + when(userRepository.findById(2L)).thenReturn(Optional.empty()); + + User result = userService.findUserById(2L); + + assertNull(result); + verify(userRepository).findById(2L); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 319285f..1489fd1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,8 @@ services: retries: 5 volumes: - mysql_data:/var/lib/mysql + - ./init:/docker-entrypoint-initdb.d + greentrip: build: @@ -29,6 +31,8 @@ services: SPRING_DATASOURCE_URL: jdbc:mysql://mysql-docker:3306/greentrip SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root + SECRET_STRIPE_KEY: secret_key_here + phpmyadmin: image: phpmyadmin:latest diff --git a/init/init.sql b/init/init.sql new file mode 100644 index 0000000..f678295 --- /dev/null +++ b/init/init.sql @@ -0,0 +1,3 @@ +CREATE DATABASE IF NOT EXISTS GREENTRIP; + +USE GREENTRIP; \ No newline at end of file diff --git a/init/seed-data.sh b/init/seed-data.sh new file mode 100644 index 0000000..c89631a --- /dev/null +++ b/init/seed-data.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +echo "⏳ Attente de 20 secondes pour s'assurer que Spring Boot a créé les tables..." +sleep 20 + +echo "🚀 Exécution des requêtes d’initialisation dans MySQL..." + +docker exec -i mysql-docker mysql -uroot -proot greentrip <<'EOF' +-- Produits +INSERT INTO product (name, description, price, image_url, stock) +SELECT * FROM (SELECT 'Sac à dos 50L', 'Sac de randonnée 50L imperméable, dos ventilé et sangles de compression.', 89.99, 'sac-a-dos-50l.jpg', 12) AS tmp +WHERE NOT EXISTS (SELECT 1 FROM product WHERE name = 'Sac à dos 50L') LIMIT 1; + +INSERT INTO product (name, description, price, image_url, stock) +SELECT * FROM (SELECT 'Chaussures de marche', 'Chaussures de randonnée haute avec semelle anti-dérapante.', 74.90, 'chaussures-marche.jpg', 20) AS tmp +WHERE NOT EXISTS (SELECT 1 FROM product WHERE name = 'Chaussures de marche') LIMIT 1; + +INSERT INTO product (name, description, price, image_url, stock) +SELECT * FROM (SELECT 'Bâtons de randonnée', 'Paire de bâtons télescopiques en aluminium, poignées ergonomiques.', 39.99, 'batons-randonnee.jpg', 30) AS tmp +WHERE NOT EXISTS (SELECT 1 FROM product WHERE name = 'Bâtons de randonnée') LIMIT 1; + +INSERT INTO product (name, description, price, image_url, stock) +SELECT * FROM (SELECT 'Tente 2 places', 'Tente légère pour 2 personnes, montage facile, résistante au vent.', 129.00, 'tente-2-places.jpg', 8) AS tmp +WHERE NOT EXISTS (SELECT 1 FROM product WHERE name = 'Tente 2 places') LIMIT 1; + +INSERT INTO product (name, description, price, image_url, stock) +SELECT * FROM (SELECT 'Lampe frontale LED', 'Lampe frontale rechargeable avec mode SOS et intensité réglable.', 24.50, 'lampe-frontale.jpg', 25) AS tmp +WHERE NOT EXISTS (SELECT 1 FROM product WHERE name = 'Lampe frontale LED') LIMIT 1; + +INSERT INTO product (name, description, price, image_url, stock) +SELECT * FROM (SELECT 'Veste imperméable', 'Veste coupe-vent imperméable, respirante et compacte.', 59.95, 'veste-impermeable.jpg', 15) AS tmp +WHERE NOT EXISTS (SELECT 1 FROM product WHERE name = 'Veste imperméable') LIMIT 1; + +INSERT INTO product (name, description, price, image_url, stock) +SELECT * FROM (SELECT 'Réchaud portable', 'Mini réchaud à gaz pliable, idéal pour bivouac et randonnée.', 22.99, 'rechaud-portable.jpg', 18) AS tmp +WHERE NOT EXISTS (SELECT 1 FROM product WHERE name = 'Réchaud portable') LIMIT 1; + +INSERT INTO product (name, description, price, image_url, stock) +SELECT * FROM (SELECT 'Bouteille filtrante', 'Gourde avec filtre à eau intégré, 650ml, pour eau de source.', 34.90, 'bouteille-filtrante.jpg', 22) AS tmp +WHERE NOT EXISTS (SELECT 1 FROM product WHERE name = 'Bouteille filtrante') LIMIT 1; + +INSERT INTO product (name, description, price, image_url, stock) +SELECT * FROM (SELECT 'Couteau multifonction', 'Outil 15-en-1 : couteau, tournevis, ouvre-boîte, ciseaux...', 19.90, 'couteau-multifonction.jpg', 40) AS tmp +WHERE NOT EXISTS (SELECT 1 FROM product WHERE name = 'Couteau multifonction') LIMIT 1; + +INSERT INTO product (name, description, price, image_url, stock) +SELECT * FROM (SELECT 'Poncho de pluie', 'Poncho ultra-léger, pliable avec capuche, taille universelle.', 14.99, 'poncho-pluie.jpg', 35) AS tmp +WHERE NOT EXISTS (SELECT 1 FROM product WHERE name = 'Poncho de pluie') LIMIT 1; +EOF + +echo "✅ Données initiales insérées avec succès !"