diff --git a/Dockerfile b/Dockerfile index 217fc15..0657c1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,17 @@ # Builder stage -FROM openjdk:21-jdk-slim as BUILDER -WORKDIR application +FROM openjdk:21-jdk-slim as builder +WORKDIR /application ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} application.jar RUN java -Djarmode=layertools -jar application.jar extract -#Final stage +# Final stage FROM openjdk:21-jdk-slim -WORKDIR Application -COPY --from=builder application/dependencies/ ./ -COPY --from=builder application/spring-boot-loader/ ./ -COPY --from=builder application/snapshot-dependencies/ ./ -COPY --from=builder application/application/ ./ -ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"] +WORKDIR /application +COPY --from=builder /application/dependencies/ ./ +COPY --from=builder /application/spring-boot-loader/ ./ +COPY --from=builder /application/snapshot-dependencies/ ./ +COPY --from=builder /application/application/ ./ +ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] EXPOSE 8080 diff --git a/README.md b/README.md new file mode 100644 index 0000000..68ddb92 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +๐Ÿ“š Online Book Store API +A comprehensive backend application for managing an online bookstore, built with Java and Spring Boot. It offers full CRUD functionality for books, categories, shopping carts, and orders, along with user authentication and role-based access control. + +--- + +๐ŸŽฏ Project Overview +This application serves as the backend for an online book store, supporting two roles: + +โ€ข **Shopper (User)** +- Sign up / Sign in +- Browse books by category or all at once +- View book details +- Add/remove books to/from cart +- Place orders and view order history + +โ€ข **Manager (Admin)** +- Add, update, delete books +- Manage categories +- Update order status + +The system is structured around 8 core domain models: +`User`, `Role`, `Book`, `Category`, `ShoppingCart`, `CartItem`, `Order`, `OrderItem` + +--- + +๐Ÿš€ Technologies & Tools + +| Tool | Description | +|--------------------|----------------------------------| +| Java 21 | Core language | +| Spring Boot 3.4.1 | Main framework | +| Spring Security | Authentication & authorization | +| Spring Data JPA | Data persistence | +| JWT | Token-based authentication | +| MapStruct | DTO mapping | +| Liquibase | DB schema management | +| Hibernate Validator| Input validation | +| Testcontainers | Integration tests with Docker | +| MySQL / H2 | Databases (prod/test) | +| Swagger | API documentation | +| Lombok | Less boilerplate | +| Checkstyle | Code quality | + +--- + +๐Ÿ”Œ API Endpoints Overview + +โœ… **Authentication** +- `POST /api/auth/register` โ€“ Register a new user +- `POST /api/auth/login` โ€“ Login and receive a JWT token + +๐Ÿ“š **Book** +- `GET /api/books` โ€“ List all books +- `GET /api/books/{id}` โ€“ View book by ID +- `POST /api/books` โ€“ Add a book (admin only) +- `PUT /api/books/{id}` โ€“ Update book +- `DELETE /api/books/{id}` โ€“ Delete book +- `GET /api/books/search` โ€“ Search books + +๐Ÿท๏ธ **Category** +- `POST /api/categories` โ€“ Add a category +- `GET /api/categories` โ€“ List categories +- `GET /api/categories/{id}` โ€“ View category +- `PUT /api/categories/{id}` โ€“ Update category +- `DELETE /api/categories/{id}` โ€“ Delete category +- `GET /api/categories/{id}/books` โ€“ View books in category + +๐Ÿ›’ **Shopping Cart** +- `GET /api/cart` โ€“ View cart +- `POST /api/cart` โ€“ Add item to cart +- `PUT /api/cart/cart-items/{cartItemId}` โ€“ Update item +- `DELETE /api/cart/cart-items/{cartItemId}` โ€“ Remove item + +๐Ÿ“ฆ **Order** +- `POST /api/orders` โ€“ Place an order +- `GET /api/orders` โ€“ View order history +- `GET /api/orders/{orderId}/items` โ€“ Items in an order +- `GET /api/orders/{orderId}/items/{itemId}` โ€“ View specific item +- `PATCH /api/orders/{id}` โ€“ Update order status + +--- + +๐ŸŒ Live Demo + +๐Ÿงช Swagger UI after starting the application locally: +http://localhost:8080/swagger-ui.html + +๐Ÿ“น YouTube Demo: +https://youtu.be/K64DTGeaMEM + +--- + +๐Ÿ” Default Users + +| Role | Email | Password | +|-------|---------------------|---------------| +| Admin | admin@example.com | adminExample | +| User | user@example.com | userExample | + +๐Ÿ› ๏ธ These accounts are automatically created on application startup via `DataInitializer`. + +--- + +โš™๏ธ Setup Instructions + +โœ… **Prerequisites** +- Java 17+ +- Docker +- Maven + +๐Ÿงช **Clone & Run Locally** +```bash +# Clone repo +git clone https://github.com/dedis34/SpringBoot-BookShop.git + +# Package app +./mvnw clean package + +# Run app with Docker +docker compose up diff --git a/docker-compose.yaml b/docker-compose.yaml index b8e76ee..e6021c3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,7 +14,7 @@ services: volumes: - mysql-data:/var/lib/mysql healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-pWetuop34!"] + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-p${MYSQLDB_ROOT_PASSWORD}"] interval: 30s timeout: 10s retries: 3 @@ -24,9 +24,9 @@ services: depends_on: - db restart: on-failure - image: my-app-image build: - context: ./app + context: ./ + dockerfile: Dockerfile env_file: - .env ports: @@ -36,8 +36,11 @@ services: SPRING_APPLICATION_JSON: > {"spring.datasource.url":"jdbc:mysql://db:3306/lombok?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true", "spring.datasource.username":"root", - "spring.datasource.password":"${MYSQL_ROOT_PASSWORD}", - "spring.jpa.hibernate.ddl-auto":"update", + "spring.datasource.password":"${MYSQLDB_ROOT_PASSWORD}", + "spring.jpa.hibernate.ddl-auto":"none", "spring.jpa.show-sql":"true", "spring.jpa.properties.hibernate.format_sql":"true"} JAVA_TOOL_OPTIONS: "-Djava.security.egd=file:/dev/./urandom" + +volumes: + mysql-data: diff --git a/pom.xml b/pom.xml index 441c174..8a96a9e 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ SpringBoot-BookShop - 21 + 21 checkstyle.xml 0.11.5 1.19.4 @@ -69,17 +69,9 @@ com.mysql mysql-connector-j - 8.2.0 runtime - - - org.hibernate.orm - hibernate-core - 6.2.7.Final - - org.mapstruct @@ -110,7 +102,7 @@ org.springdoc springdoc-openapi-starter-webmvc-ui - 2.1.0 + 2.8.8 @@ -203,6 +195,29 @@ liquibase-maven-plugin 4.23.0 + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.0 + + + + org.example.SpringBootBookShopApplication + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + true + + + @@ -210,9 +225,6 @@ with-liquibase - - true - @@ -237,4 +249,4 @@ - + \ No newline at end of file diff --git a/src/main/java/org/example/config/SecurityConfig.java b/src/main/java/org/example/config/SecurityConfig.java index 67977e5..ee97c85 100644 --- a/src/main/java/org/example/config/SecurityConfig.java +++ b/src/main/java/org/example/config/SecurityConfig.java @@ -8,6 +8,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -16,6 +17,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration +@EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { private final CustomUserDetailsServiceImpl customUserDetailServiceImpl; @@ -33,7 +35,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests( auth -> auth - .requestMatchers("/auth/**", "/error", "/swagger-ui/**", "/v3/api-docs/**") + .requestMatchers("/api/auth/**", "/error", "/swagger-ui/**", "/v3/api-docs/**") .permitAll() .anyRequest() .authenticated() diff --git a/src/main/java/org/example/controller/BookController.java b/src/main/java/org/example/controller/BookController.java index 2ec8aff..a2c3f4a 100644 --- a/src/main/java/org/example/controller/BookController.java +++ b/src/main/java/org/example/controller/BookController.java @@ -28,7 +28,7 @@ @Tag(name = "Book management", description = "Endpoints for managing books") @RequiredArgsConstructor @RestController -@RequestMapping(value = "/books") +@RequestMapping(value = "/api/books") public class BookController { private final BookService bookService; diff --git a/src/main/java/org/example/controller/ShoppingCartController.java b/src/main/java/org/example/controller/ShoppingCartController.java index 5ee9873..2756e19 100644 --- a/src/main/java/org/example/controller/ShoppingCartController.java +++ b/src/main/java/org/example/controller/ShoppingCartController.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.example.dto.cartItem.AddItemToCartRequestDto; import org.example.dto.cartItem.UpdateCartItemRequestDto; @@ -53,7 +54,7 @@ public void removeCartItem(@PathVariable Long cartItemId) { @PutMapping("/cart-items/{cartItemId}") @PreAuthorize("hasRole('ROLE_USER')") public ShoppingCartResponseDto updateCartItem(@PathVariable Long cartItemId, - @RequestBody UpdateCartItemRequestDto request) { + @Valid @RequestBody UpdateCartItemRequestDto request) { return shoppingCartServiceImpl.updateCartItem(cartItemId, request); } diff --git a/src/main/java/org/example/customAnnotations/validators/FieldMatchValidator.java b/src/main/java/org/example/customAnnotations/validators/FieldMatchValidator.java index 95d9f6b..271573b 100644 --- a/src/main/java/org/example/customAnnotations/validators/FieldMatchValidator.java +++ b/src/main/java/org/example/customAnnotations/validators/FieldMatchValidator.java @@ -2,10 +2,11 @@ import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; -import org.example.dto.user.UserRegistrationRequestDto; import org.example.customAnnotations.FieldMatch; +import org.springframework.beans.BeanWrapperImpl; + +public class FieldMatchValidator implements ConstraintValidator { -public class FieldMatchValidator implements ConstraintValidator { private String field; private String fieldMatch; @@ -16,22 +17,14 @@ public void initialize(FieldMatch constraintAnnotation) { } @Override - public boolean isValid(UserRegistrationRequestDto dto, ConstraintValidatorContext context) { - if (dto == null) { - return true; - } - - String fieldValue = getFieldValue(dto, field); - String fieldMatchValue = getFieldValue(dto, fieldMatch); + public boolean isValid(Object value, ConstraintValidatorContext context) { + Object fieldValue = new BeanWrapperImpl(value).getPropertyValue(field); + Object fieldMatchValue = new BeanWrapperImpl(value).getPropertyValue(fieldMatch); - return fieldValue != null && fieldValue.equals(fieldMatchValue); - } - - private String getFieldValue(UserRegistrationRequestDto dto, String fieldName) { - try { - return (String) dto.getClass().getDeclaredField(fieldName).get(dto); - } catch (Exception e) { - return null; + if (fieldValue == null || fieldMatchValue == null) { + return false; } + + return fieldValue.equals(fieldMatchValue); } } diff --git a/src/main/java/org/example/dto/shoppingCart/ShoppingCartResponseDto.java b/src/main/java/org/example/dto/shoppingCart/ShoppingCartResponseDto.java index 5ab115c..b509745 100644 --- a/src/main/java/org/example/dto/shoppingCart/ShoppingCartResponseDto.java +++ b/src/main/java/org/example/dto/shoppingCart/ShoppingCartResponseDto.java @@ -1,8 +1,10 @@ package org.example.dto.shoppingCart; +import lombok.Builder; import org.example.dto.cartItem.CartItemResponseDto; import java.util.Set; +@Builder public record ShoppingCartResponseDto( Long id, Long userId, diff --git a/src/main/java/org/example/dto/user/UserLoginRequestDto.java b/src/main/java/org/example/dto/user/UserLoginRequestDto.java index d1debf4..2c7b9ff 100644 --- a/src/main/java/org/example/dto/user/UserLoginRequestDto.java +++ b/src/main/java/org/example/dto/user/UserLoginRequestDto.java @@ -6,7 +6,7 @@ public record UserLoginRequestDto( @NotEmpty - @Size(min = 8, max = 20) + @Size(min = 8, max = 35) @Email String email, @NotEmpty diff --git a/src/main/java/org/example/exception/CustomGlobalExceptionHandler.java b/src/main/java/org/example/exception/CustomGlobalExceptionHandler.java index 153c332..c963cb7 100644 --- a/src/main/java/org/example/exception/CustomGlobalExceptionHandler.java +++ b/src/main/java/org/example/exception/CustomGlobalExceptionHandler.java @@ -49,11 +49,10 @@ public ResponseEntity handleRegistrationException(@NonNull RegistrationE } private String getErrorMessage(ObjectError objectError) { - if (objectError instanceof FieldError) { - String field = ((FieldError) objectError).getField(); - String defaultMessage = objectError.getDefaultMessage(); - return field + " " + defaultMessage; + if (objectError instanceof FieldError fieldError) { + return fieldError.getField() + " " + fieldError.getDefaultMessage(); + } else { + return objectError.getDefaultMessage(); } - return null; } } diff --git a/src/main/java/org/example/model/User.java b/src/main/java/org/example/model/User.java index c7dddb6..f514fd9 100644 --- a/src/main/java/org/example/model/User.java +++ b/src/main/java/org/example/model/User.java @@ -28,7 +28,7 @@ public class User implements UserDetails { private String shippingAddress; @ManyToMany(fetch = FetchType.EAGER) @JoinTable( - name = "user_roles", + name = "users_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id") ) diff --git a/src/main/java/org/example/security/filter/JwtAuthenticationFilter.java b/src/main/java/org/example/security/filter/JwtAuthenticationFilter.java index 4a0fc0d..71957bf 100644 --- a/src/main/java/org/example/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/org/example/security/filter/JwtAuthenticationFilter.java @@ -34,7 +34,7 @@ protected void doFilterInternal( if (token != null && jwtUtil.isValidToken(token)) { String username = jwtUtil.getUserName(token); - UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); // Uลผycie customUserDetailsService + UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); Authentication authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 1ba1fa6..c95cde0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,7 +5,7 @@ spring.datasource.password=Wetuop34! spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # Ustawienia JPA -spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.ddl-auto=none spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true @@ -14,5 +14,5 @@ spring.liquibase.enabled=true spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml # Konfiguracja JWT -jwt.expiration=300 +jwt.expiration=600000 jwt.secret=oajshfoliaslfas213ojhagsgd343adfadfhdfhffhsgwr543sfdgs2 diff --git a/src/main/resources/db/changelog/changes/001-create-users-table.yaml b/src/main/resources/db/changelog/changes/001-create-users-table.yaml new file mode 100644 index 0000000..dcff92c --- /dev/null +++ b/src/main/resources/db/changelog/changes/001-create-users-table.yaml @@ -0,0 +1,38 @@ +databaseChangeLog: + - changeSet: + id: 001-create-users-table + author: artur + changes: + - createTable: + tableName: users + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: email + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: first_name + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: last_name + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: password + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: shipping_address + type: VARCHAR(255) \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/001-insert-users.yaml b/src/main/resources/db/changelog/changes/001-insert-users.yaml deleted file mode 100644 index 8252a8d..0000000 --- a/src/main/resources/db/changelog/changes/001-insert-users.yaml +++ /dev/null @@ -1,27 +0,0 @@ -databaseChangeLog: - - changeSet: - id: 001-insert-users - author: artur - preConditions: - - onFail: MARK_RAN - - tableExists: - tableName: users - changes: - - insert: - tableName: users - columns: - - column: - name: id - valueNumeric: "1" - - column: - name: email - value: admin@example.com - - column: - name: password - value: $2a$10$XyZ3wQ9E1PJKL6VjFdfSseCEq9rOvhA/UNmV94QHqkG2l6bBZyV6G - - column: - name: first_name - value: Alice - - column: - name: last_name - value: Stone \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/002-create-roles-table.yaml b/src/main/resources/db/changelog/changes/002-create-roles-table.yaml new file mode 100644 index 0000000..b9ac842 --- /dev/null +++ b/src/main/resources/db/changelog/changes/002-create-roles-table.yaml @@ -0,0 +1,20 @@ +databaseChangeLog: + - changeSet: + id: 002-create-roles-table + author: artur + changes: + - createTable: + tableName: roles + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: name + type: VARCHAR(255) + constraints: + nullable: false \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/003-create-users-roles-table.yaml b/src/main/resources/db/changelog/changes/003-create-users-roles-table.yaml index a0a7603..a9388a7 100644 --- a/src/main/resources/db/changelog/changes/003-create-users-roles-table.yaml +++ b/src/main/resources/db/changelog/changes/003-create-users-roles-table.yaml @@ -2,13 +2,6 @@ databaseChangeLog: - changeSet: id: 003-create-users-roles-table author: artur - preConditions: - - onFail: MARK_RAN - - not: - - tableExists: - tableName: users_roles - - tableExists: - tableName: roles changes: - createTable: tableName: users_roles @@ -18,18 +11,24 @@ databaseChangeLog: type: BIGINT constraints: nullable: false - foreignKeyName: fk_users_roles_user - referencedTableName: users - referencedColumnNames: id - column: name: role_id type: BIGINT constraints: nullable: false - foreignKeyName: fk_users_roles_role - referencedTableName: roles - referencedColumnNames: id - addPrimaryKey: tableName: users_roles columnNames: user_id, role_id constraintName: pk_users_roles + - addForeignKeyConstraint: + baseTableName: users_roles + baseColumnNames: user_id + constraintName: fk_users_roles_user + referencedTableName: users + referencedColumnNames: id + - addForeignKeyConstraint: + baseTableName: users_roles + baseColumnNames: role_id + constraintName: fk_users_roles_role + referencedTableName: roles + referencedColumnNames: id \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/004-assign-roles.yaml b/src/main/resources/db/changelog/changes/004-assign-roles.yaml deleted file mode 100644 index dc39755..0000000 --- a/src/main/resources/db/changelog/changes/004-assign-roles.yaml +++ /dev/null @@ -1,27 +0,0 @@ -databaseChangeLog: - - changeSet: - id: 004-assign-roles - author: artur - preConditions: - - onFail: MARK_RAN - - tableExists: - tableName: users_roles - changes: - - insert: - tableName: users_roles - columns: - - column: - name: user_id - valueNumeric: "1" - - column: - name: role_id - valueNumeric: "1" - - insert: - tableName: users_roles - columns: - - column: - name: user_id - valueNumeric: "2" - - column: - name: role_id - valueNumeric: "2" diff --git a/src/main/resources/db/changelog/changes/005-create-categories-table.yaml b/src/main/resources/db/changelog/changes/004-create-categories-table.yaml similarity index 65% rename from src/main/resources/db/changelog/changes/005-create-categories-table.yaml rename to src/main/resources/db/changelog/changes/004-create-categories-table.yaml index c59ce98..5f5504a 100644 --- a/src/main/resources/db/changelog/changes/005-create-categories-table.yaml +++ b/src/main/resources/db/changelog/changes/004-create-categories-table.yaml @@ -1,6 +1,6 @@ databaseChangeLog: - changeSet: - id: 005-create-categories-table + id: 004-create-categories-table author: artur changes: - createTable: @@ -8,23 +8,20 @@ databaseChangeLog: columns: - column: name: id - type: bigint + type: BIGINT autoIncrement: true constraints: primaryKey: true nullable: false - column: name: name - type: varchar(255) + type: VARCHAR(255) constraints: nullable: false - column: name: description - type: varchar(255) - constraints: - nullable: true + type: VARCHAR(255) - column: name: is_deleted - type: boolean - constraints: - nullable: false + type: BOOLEAN + defaultValueBoolean: false \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/006-insert-categories.yaml b/src/main/resources/db/changelog/changes/005-insert-categories.yaml similarity index 56% rename from src/main/resources/db/changelog/changes/006-insert-categories.yaml rename to src/main/resources/db/changelog/changes/005-insert-categories.yaml index ccb342b..95d75ef 100644 --- a/src/main/resources/db/changelog/changes/006-insert-categories.yaml +++ b/src/main/resources/db/changelog/changes/005-insert-categories.yaml @@ -1,17 +1,20 @@ databaseChangeLog: - changeSet: - id: 006-insert-categories + id: 005-insert-categories author: artur changes: - insert: tableName: categories columns: + - column: + name: id + valueNumeric: 1 - column: name: name - value: "Fiction" + value: "Programming" - column: name: description - value: "Fiction books" + value: "Books about programming" - column: name: is_deleted - valueBoolean: false + valueBoolean: false \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/006-insert-users.yaml b/src/main/resources/db/changelog/changes/006-insert-users.yaml new file mode 100644 index 0000000..e12c75d --- /dev/null +++ b/src/main/resources/db/changelog/changes/006-insert-users.yaml @@ -0,0 +1,41 @@ +databaseChangeLog: + - changeSet: + id: 006-insert-users + author: artur + changes: + - insert: + tableName: users + columns: + - column: + name: first_name + value: "Admin" + - column: + name: last_name + value: "Admin" + - column: + name: email + value: "admin@example.com" + - column: + name: password + value: "$2a$10$EQVI2UweY22CoQXOd4I3EOF41lk55sboVMYQ7sgroVveCyR6wviZC" + - column: + name: shipping_address + value: "random Adress Admin" + - insert: + tableName: users + columns: + - column: + name: first_name + value: "User" + - column: + name: last_name + value: "User" + - column: + name: email + value: "user@example.com" + - column: + name: password + value: "$2a$10$DZKy0ba28TVjZDrvY9hcpu9jIq2DlXrduGWpvPHg6Tt1.9UWYgiSC" + - column: + name: shipping_address + value: "random Adress User" \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/002-insert-roles.yaml b/src/main/resources/db/changelog/changes/007-insert-roles.yaml similarity index 60% rename from src/main/resources/db/changelog/changes/002-insert-roles.yaml rename to src/main/resources/db/changelog/changes/007-insert-roles.yaml index 3e76b68..cbb6a95 100644 --- a/src/main/resources/db/changelog/changes/002-insert-roles.yaml +++ b/src/main/resources/db/changelog/changes/007-insert-roles.yaml @@ -1,27 +1,23 @@ databaseChangeLog: - changeSet: - id: 002-insert-roles + id: 007-insert-roles author: artur - preConditions: - - onFail: MARK_RAN - - tableExists: - tableName: roles changes: - insert: tableName: roles columns: - column: name: id - valueNumeric: "1" + valueNumeric: 1 - column: name: name - value: ROLE_ADMIN + value: "ROLE_USER" - insert: tableName: roles columns: - column: name: id - valueNumeric: "2" + valueNumeric: 2 - column: name: name - value: ROLE_USER + value: "ROLE_ADMIN" \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/008-assign-roles.yaml b/src/main/resources/db/changelog/changes/008-assign-roles.yaml new file mode 100644 index 0000000..8824937 --- /dev/null +++ b/src/main/resources/db/changelog/changes/008-assign-roles.yaml @@ -0,0 +1,20 @@ +databaseChangeLog: + - changeSet: + id: 008-assign-roles + author: artur + changes: + - sql: + sql: > + INSERT INTO users_roles (user_id, role_id) + SELECT u.id, r.id FROM users u, roles r + WHERE u.email = 'admin@example.com' AND r.name = 'ROLE_ADMIN'; + - sql: + sql: > + INSERT INTO users_roles (user_id, role_id) + SELECT u.id, r.id FROM users u, roles r + WHERE u.email = 'admin@example.com' AND r.name = 'ROLE_USER'; + - sql: + sql: > + INSERT INTO users_roles (user_id, role_id) + SELECT u.id, r.id FROM users u, roles r + WHERE u.email = 'user@example.com' AND r.name = 'ROLE_USER'; \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/009-create-books-table.yaml b/src/main/resources/db/changelog/changes/009-create-books-table.yaml new file mode 100644 index 0000000..ee1a5ce --- /dev/null +++ b/src/main/resources/db/changelog/changes/009-create-books-table.yaml @@ -0,0 +1,46 @@ +databaseChangeLog: + - changeSet: + id: 009-create-books-table + author: artur + changes: + - createTable: + tableName: books + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: title + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: author + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: isbn + type: VARCHAR(255) + constraints: + nullable: false + unique: true + - column: + name: price + type: DECIMAL(10,2) + constraints: + nullable: false + - column: + name: description + type: VARCHAR(1000) + - column: + name: cover_image + type: VARCHAR(255) + - column: + name: is_deleted + type: BOOLEAN + defaultValueBoolean: false diff --git a/src/main/resources/db/changelog/changes/010-create-book-category-table.yaml b/src/main/resources/db/changelog/changes/010-create-book-category-table.yaml new file mode 100644 index 0000000..d56a33c --- /dev/null +++ b/src/main/resources/db/changelog/changes/010-create-book-category-table.yaml @@ -0,0 +1,34 @@ +databaseChangeLog: + - changeSet: + id: 010-create-book-category-table + author: artur + changes: + - createTable: + tableName: book_category + columns: + - column: + name: book_id + type: BIGINT + constraints: + nullable: false + - column: + name: category_id + type: BIGINT + constraints: + nullable: false + - addPrimaryKey: + tableName: book_category + columnNames: book_id, category_id + constraintName: pk_book_category + - addForeignKeyConstraint: + baseTableName: book_category + baseColumnNames: book_id + referencedTableName: books + referencedColumnNames: id + constraintName: fk_book_category_book + - addForeignKeyConstraint: + baseTableName: book_category + baseColumnNames: category_id + referencedTableName: categories + referencedColumnNames: id + constraintName: fk_book_category_category diff --git a/src/main/resources/db/changelog/changes/011-create-shopping-carts-table.yaml b/src/main/resources/db/changelog/changes/011-create-shopping-carts-table.yaml new file mode 100644 index 0000000..c46ed1e --- /dev/null +++ b/src/main/resources/db/changelog/changes/011-create-shopping-carts-table.yaml @@ -0,0 +1,26 @@ +databaseChangeLog: + - changeSet: + id: 011-create-shopping-carts-table + author: artur + changes: + - createTable: + tableName: shopping_carts + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: user_id + type: BIGINT + constraints: + nullable: false + - addForeignKeyConstraint: + baseTableName: shopping_carts + baseColumnNames: user_id + referencedTableName: users + referencedColumnNames: id + constraintName: fk_shopping_cart_user diff --git a/src/main/resources/db/changelog/changes/012-create-cart-items-table.yaml b/src/main/resources/db/changelog/changes/012-create-cart-items-table.yaml new file mode 100644 index 0000000..fc811fe --- /dev/null +++ b/src/main/resources/db/changelog/changes/012-create-cart-items-table.yaml @@ -0,0 +1,42 @@ +databaseChangeLog: + - changeSet: + id: 012-create-cart-items-table + author: artur + changes: + - createTable: + tableName: cart_items + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: shopping_cart_id + type: BIGINT + constraints: + nullable: false + - column: + name: book_id + type: BIGINT + constraints: + nullable: false + - column: + name: quantity + type: INT + constraints: + nullable: false + - addForeignKeyConstraint: + baseTableName: cart_items + baseColumnNames: shopping_cart_id + referencedTableName: shopping_carts + referencedColumnNames: id + constraintName: fk_cart_items_cart + - addForeignKeyConstraint: + baseTableName: cart_items + baseColumnNames: book_id + referencedTableName: books + referencedColumnNames: id + constraintName: fk_cart_items_book diff --git a/src/main/resources/db/changelog/changes/013-create-orders-table.yaml b/src/main/resources/db/changelog/changes/013-create-orders-table.yaml new file mode 100644 index 0000000..e40af2b --- /dev/null +++ b/src/main/resources/db/changelog/changes/013-create-orders-table.yaml @@ -0,0 +1,46 @@ +databaseChangeLog: + - changeSet: + id: 013-create-orders-table + author: artur + changes: + - createTable: + tableName: orders + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: user_id + type: BIGINT + constraints: + nullable: false + - column: + name: status + type: VARCHAR(50) + constraints: + nullable: false + - column: + name: total + type: DECIMAL(10,2) + constraints: + nullable: false + - column: + name: shipping_address + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: order_date + type: DATETIME + constraints: + nullable: false + - addForeignKeyConstraint: + baseTableName: orders + baseColumnNames: user_id + referencedTableName: users + referencedColumnNames: id + constraintName: fk_orders_user diff --git a/src/main/resources/db/changelog/changes/014-create-order-items-table.yaml b/src/main/resources/db/changelog/changes/014-create-order-items-table.yaml new file mode 100644 index 0000000..23db12b --- /dev/null +++ b/src/main/resources/db/changelog/changes/014-create-order-items-table.yaml @@ -0,0 +1,47 @@ +databaseChangeLog: + - changeSet: + id: 014-create-order-items-table + author: artur + changes: + - createTable: + tableName: order_items + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: order_id + type: BIGINT + constraints: + nullable: false + - column: + name: book_id + type: BIGINT + constraints: + nullable: false + - column: + name: quantity + type: INT + constraints: + nullable: false + - column: + name: price + type: DECIMAL(10,2) + constraints: + nullable: false + - addForeignKeyConstraint: + baseTableName: order_items + baseColumnNames: order_id + referencedTableName: orders + referencedColumnNames: id + constraintName: fk_order_items_order + - addForeignKeyConstraint: + baseTableName: order_items + baseColumnNames: book_id + referencedTableName: books + referencedColumnNames: id + constraintName: fk_order_items_book diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 5676852..1af39ff 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -1,13 +1,29 @@ databaseChangeLog: - include: - file: db/changelog/changes/001-insert-users.yaml + file: db/changelog/changes/001-create-users-table.yaml - include: - file: db/changelog/changes/002-insert-roles.yaml + file: db/changelog/changes/002-create-roles-table.yaml - include: file: db/changelog/changes/003-create-users-roles-table.yaml - include: - file: db/changelog/changes/004-assign-roles.yaml + file: db/changelog/changes/004-create-categories-table.yaml - include: - file: db/changelog/changes/005-create-categories-table.yaml + file: db/changelog/changes/005-insert-categories.yaml - include: - file: db/changelog/changes/006-insert-categories.yaml + file: db/changelog/changes/006-insert-users.yaml + - include: + file: db/changelog/changes/007-insert-roles.yaml + - include: + file: db/changelog/changes/008-assign-roles.yaml + - include: + file: db/changelog/changes/009-create-books-table.yaml + - include: + file: db/changelog/changes/010-create-book-category-table.yaml + - include: + file: db/changelog/changes/011-create-shopping-carts-table.yaml + - include: + file: db/changelog/changes/012-create-cart-items-table.yaml + - include: + file: db/changelog/changes/013-create-orders-table.yaml + - include: + file: db/changelog/changes/014-create-order-items-table.yaml \ No newline at end of file diff --git a/src/test/java/org/example/controller/ShoppingCartControllerTest.java b/src/test/java/org/example/controller/ShoppingCartControllerTest.java index d111e11..184cf2b 100644 --- a/src/test/java/org/example/controller/ShoppingCartControllerTest.java +++ b/src/test/java/org/example/controller/ShoppingCartControllerTest.java @@ -21,10 +21,12 @@ import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import com.fasterxml.jackson.databind.ObjectMapper; class ShoppingCartControllerTest { private MockMvc mockMvc; + private ObjectMapper objectMapper; @Mock private ShoppingCartServiceImpl shoppingCartServiceImpl; @@ -38,6 +40,7 @@ class ShoppingCartControllerTest { @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); + objectMapper = new ObjectMapper(); mockUser = new User(); mockUser.setId(1L); @@ -58,7 +61,7 @@ void updateCartItem_WhenCartItemExists_ShouldUpdateQuantity() throws Exception { mockMvc.perform(put("/api/cart/cart-items/1") .contentType(MediaType.APPLICATION_JSON) - .content("{\"quantity\":3}")) + .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1L)); diff --git a/src/test/java/org/example/service/ShoppingCartServiceTest.java b/src/test/java/org/example/service/ShoppingCartServiceTest.java index 08af7c6..035915d 100644 --- a/src/test/java/org/example/service/ShoppingCartServiceTest.java +++ b/src/test/java/org/example/service/ShoppingCartServiceTest.java @@ -1,8 +1,10 @@ package org.example.service; import org.example.dto.cartItem.AddItemToCartRequestDto; +import org.example.dto.cartItem.CartItemResponseDto; import org.example.dto.cartItem.UpdateCartItemRequestDto; import org.example.dto.shoppingCart.ShoppingCartResponseDto; +import org.example.exception.BookNotFoundException; import org.example.exception.CartItemNotFoundException; import org.example.exception.UserNotFoundException; import org.example.mapper.ShoppingCartMapper; @@ -22,6 +24,7 @@ import org.mockito.MockitoAnnotations; import java.util.HashSet; import java.util.Optional; +import java.util.Set; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -74,12 +77,26 @@ void setUp() { cartItem.setQuantity(2); cartItem.setShoppingCart(shoppingCart); - shoppingCartResponseDto = new ShoppingCartResponseDto(1L, 1L, new HashSet<>()); + CartItemResponseDto itemDto = CartItemResponseDto.builder() + .id(1L) + .bookId(1L) + .bookTitle("Test Book") + .quantity(2) + .build(); + + Set cartItems = Set.of(itemDto); + + shoppingCartResponseDto = ShoppingCartResponseDto.builder() + .id(1L) + .userId(1L) + .cartItems(cartItems) + .build(); } @Test void addToCart_WhenUserExistsAndBookExists_ShouldAddItemToCart() { AddItemToCartRequestDto request = new AddItemToCartRequestDto(1L, 2); + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); when(shoppingCartRepository.findByUser(user)).thenReturn(Optional.of(shoppingCart)); when(bookRepository.findById(1L)).thenReturn(Optional.of(book)); @@ -88,6 +105,16 @@ void addToCart_WhenUserExistsAndBookExists_ShouldAddItemToCart() { ShoppingCartResponseDto result = shoppingCartService.addToCart(1L, request); assertNotNull(result); + assertEquals(1L, result.userId()); + assertEquals(shoppingCart.getId(), result.id()); + assertNotNull(result.cartItems()); + assertEquals(1, result.cartItems().size()); + + CartItemResponseDto cartItemResponseDto = result.cartItems().iterator().next(); + assertEquals(1L, cartItemResponseDto.bookId()); + assertEquals("Test Book", cartItemResponseDto.bookTitle()); + assertEquals(2, cartItemResponseDto.quantity()); + verify(cartItemRepository).save(any(CartItem.class)); } @@ -155,4 +182,16 @@ void removeCartItem_WhenItemNotFound_ShouldThrowException() { assertThrows(CartItemNotFoundException.class, () -> shoppingCartService.removeCartItem(1L)); } + + @Test + void addToCart_WhenBookNotFound_ShouldThrowException() { + AddItemToCartRequestDto request = new AddItemToCartRequestDto(1L, 2); + + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + when(shoppingCartRepository.findByUser(user)).thenReturn(Optional.of(shoppingCart)); + when(bookRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThrows(BookNotFoundException.class, () -> shoppingCartService.addToCart(1L, request)); + } + }