├── infrastructure ├── src │ ├── main │ │ ├── resources │ │ │ ├── db │ │ │ │ └── migration │ │ │ │ │ ├── U1__Initial.sql │ │ │ │ │ ├── U2__Create_Genre_Aggregate.sql │ │ │ │ │ ├── V1__Initial.sql │ │ │ │ │ └── V2__Create_Genre_Aggregate.sql │ │ │ ├── application-test-e2e.yml │ │ │ ├── application-development.yml │ │ │ ├── application-production.yml │ │ │ ├── application-test-integration.yml │ │ │ └── application.yml │ │ └── java │ │ │ └── com │ │ │ └── fullcycle │ │ │ └── admin │ │ │ └── catalogo │ │ │ └── infrastructure │ │ │ ├── configuration │ │ │ ├── WebServerConfig.java │ │ │ ├── ObjectMapperConfig.java │ │ │ ├── usecases │ │ │ │ ├── CategoryUseCaseConfig.java │ │ │ │ └── GenreUseCaseConfig.java │ │ │ └── json │ │ │ │ └── Json.java │ │ │ ├── category │ │ │ ├── models │ │ │ │ ├── CreateCategoryRequest.java │ │ │ │ ├── UpdateCategoryRequest.java │ │ │ │ ├── CategoryListResponse.java │ │ │ │ └── CategoryResponse.java │ │ │ ├── persistence │ │ │ │ ├── CategoryRepository.java │ │ │ │ └── CategoryJpaEntity.java │ │ │ ├── presenters │ │ │ │ └── CategoryApiPresenter.java │ │ │ └── CategoryMySQLGateway.java │ │ │ ├── genre │ │ │ ├── persistence │ │ │ │ ├── GenreRepository.java │ │ │ │ ├── GenreCategoryJpaEntity.java │ │ │ │ ├── GenreCategoryID.java │ │ │ │ └── GenreJpaEntity.java │ │ │ └── GenreMySQLGateway.java │ │ │ ├── utils │ │ │ └── SpecificationUtils.java │ │ │ ├── Main.java │ │ │ └── api │ │ │ ├── controllers │ │ │ ├── GlobalExceptionHandler.java │ │ │ └── CategoryController.java │ │ │ └── CategoryAPI.java │ └── test │ │ └── java │ │ └── com │ │ └── fullcycle │ │ └── admin │ │ └── catalogo │ │ ├── IntegrationTest.java │ │ ├── E2ETest.java │ │ ├── JacksonTest.java │ │ ├── ControllerTest.java │ │ ├── MySQLGatewayTest.java │ │ ├── MySQLCleanUpExtension.java │ │ ├── infrastructure │ │ └── category │ │ │ ├── models │ │ │ ├── UpdateCategoryRequestTest.java │ │ │ ├── CategoryListResponseTest.java │ │ │ ├── CreateCategoryRequestTest.java │ │ │ └── CategoryResponseTest.java │ │ │ └── persistence │ │ │ └── CategoryRepositoryTest.java │ │ └── application │ │ ├── genre │ │ ├── delete │ │ │ └── DeleteGenreUseCaseIT.java │ │ └── retrieve │ │ │ ├── get │ │ │ └── GetGenreByIdUseCaseIT.java │ │ │ └── list │ │ │ └── ListGenreUseCaseIT.java │ │ └── category │ │ ├── delete │ │ └── DeleteCategoryUseCaseIT.java │ │ ├── retrieve │ │ ├── get │ │ │ └── GetCategoryByIdUseCaseIT.java │ │ └── list │ │ │ └── ListCategoriesUseCaseIT.java │ │ ├── create │ │ └── CreateCategoryUseCaseIT.java │ │ └── update │ │ └── UpdateCategoryUseCaseIT.java └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle ├── domain ├── src │ └── main │ │ └── java │ │ └── com │ │ └── fullcycle │ │ └── admin │ │ └── catalogo │ │ └── domain │ │ ├── ValueObject.java │ │ ├── validation │ │ ├── Error.java │ │ ├── Validator.java │ │ ├── ValidationHandler.java │ │ └── handler │ │ │ ├── ThrowsValidationHandler.java │ │ │ └── Notification.java │ │ ├── Identifier.java │ │ ├── AggregateRoot.java │ │ ├── pagination │ │ ├── SearchQuery.java │ │ └── Pagination.java │ │ ├── utils │ │ └── InstantUtils.java │ │ ├── exceptions │ │ ├── NotificationException.java │ │ ├── NoStacktraceException.java │ │ ├── DomainException.java │ │ └── NotFoundException.java │ │ ├── genre │ │ ├── GenreGateway.java │ │ ├── GenreID.java │ │ ├── GenreValidator.java │ │ └── Genre.java │ │ ├── category │ │ ├── CategoryGateway.java │ │ ├── CategoryID.java │ │ ├── CategoryValidator.java │ │ └── Category.java │ │ └── Entity.java └── build.gradle ├── application ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── fullcycle │ │ │ └── admin │ │ │ └── catalogo │ │ │ └── application │ │ │ ├── UseCase.java │ │ │ ├── NullaryUseCase.java │ │ │ ├── UnitUseCase.java │ │ │ ├── genre │ │ │ ├── delete │ │ │ │ ├── DeleteGenreUseCase.java │ │ │ │ └── DefaultDeleteGenreUseCase.java │ │ │ ├── retrieve │ │ │ │ ├── get │ │ │ │ │ ├── GetGenreByIdUseCase.java │ │ │ │ │ ├── DefaultGetGenreByIdUseCase.java │ │ │ │ │ └── GenreOutput.java │ │ │ │ └── list │ │ │ │ │ ├── ListGenreUseCase.java │ │ │ │ │ ├── DefaultListGenreUseCase.java │ │ │ │ │ └── GenreListOutput.java │ │ │ ├── create │ │ │ │ ├── CreateGenreUseCase.java │ │ │ │ ├── CreateGenreOutput.java │ │ │ │ ├── CreateGenreCommand.java │ │ │ │ └── DefaultCreateGenreUseCase.java │ │ │ └── update │ │ │ │ ├── UpdateGenreUseCase.java │ │ │ │ ├── UpdateGenreOutput.java │ │ │ │ ├── UpdateGenreCommand.java │ │ │ │ └── DefaultUpdateGenreUseCase.java │ │ │ └── category │ │ │ ├── delete │ │ │ ├── DeleteCategoryUseCase.java │ │ │ └── DefaultDeleteCategoryUseCase.java │ │ │ ├── retrieve │ │ │ ├── get │ │ │ │ ├── GetCategoryByIdUseCase.java │ │ │ │ ├── CategoryOutput.java │ │ │ │ └── DefaultGetCategoryByIdUseCase.java │ │ │ └── list │ │ │ │ ├── ListCategoriesUseCase.java │ │ │ │ ├── DefaultListCategoriesUseCase.java │ │ │ │ └── CategoryListOutput.java │ │ │ ├── create │ │ │ ├── CreateCategoryUseCase.java │ │ │ ├── CreateCategoryCommand.java │ │ │ ├── CreateCategoryOutput.java │ │ │ └── DefaultCreateCategoryUseCase.java │ │ │ └── update │ │ │ ├── UpdateCategoryUseCase.java │ │ │ ├── UpdateCategoryOutput.java │ │ │ ├── UpdateCategoryCommand.java │ │ │ └── DefaultUpdateCategoryUseCase.java │ └── test │ │ └── java │ │ └── com │ │ └── fullcycle │ │ └── admin │ │ └── catalogo │ │ └── application │ │ ├── UseCaseTest.java │ │ ├── category │ │ ├── delete │ │ │ └── DeleteCategoryUseCaseTest.java │ │ ├── retrieve │ │ │ ├── get │ │ │ │ └── GetCategoryByIdUseCaseTest.java │ │ │ └── list │ │ │ │ └── ListCategoriesUseCaseTest.java │ │ └── create │ │ │ └── CreateCategoryUseCaseTest.java │ │ └── genre │ │ ├── delete │ │ └── DeleteGenreUseCaseTest.java │ │ └── retrieve │ │ ├── get │ │ └── GetGenreByIdUseCaseTest.java │ │ └── list │ │ └── ListGenreUseCaseTest.java └── build.gradle ├── docker-compose.yml ├── .gitignore ├── gradlew.bat └── README.md /infrastructure/src/main/resources/db/migration/U1__Initial.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP TABLE category; -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeedu/micro-admin-videos-java/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'fc3-admin-do-catalogo' 2 | include 'domain' 3 | include 'application' 4 | include 'infrastructure' -------------------------------------------------------------------------------- /infrastructure/src/main/resources/db/migration/U2__Create_Genre_Aggregate.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP TABLE genres; 3 | DROP TABLE genres_categories; -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/ValueObject.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain; 2 | 3 | public abstract class ValueObject { 4 | } 5 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/validation/Error.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.validation; 2 | 3 | public record Error(String message) { 4 | } 5 | -------------------------------------------------------------------------------- /infrastructure/src/main/resources/application-test-e2e.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | ROOT: info 4 | 5 | mysql: 6 | username: root 7 | password: 123456 8 | schema: adm_videos 9 | url: localhost:${mysql.port} -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/UseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application; 2 | 3 | public abstract class UseCase { 4 | 5 | public abstract OUT execute(IN anIn); 6 | } -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/NullaryUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application; 2 | 3 | public abstract class NullaryUseCase { 4 | 5 | public abstract OUT execute(); 6 | } 7 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/Identifier.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain; 2 | 3 | public abstract class Identifier extends ValueObject { 4 | 5 | public abstract String getValue(); 6 | } 7 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/UnitUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application; 2 | 3 | public abstract class UnitUseCase { 4 | 5 | public abstract void execute(IN anIn); 6 | } 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /infrastructure/src/main/resources/application-development.yml: -------------------------------------------------------------------------------- 1 | mysql: 2 | username: root 3 | password: 123456 4 | schema: adm_videos 5 | url: localhost:3306 6 | 7 | server: 8 | port: 8080 9 | undertow: 10 | threads: 11 | worker: 10 12 | io: 2 -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/AggregateRoot.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain; 2 | 3 | public abstract class AggregateRoot extends Entity { 4 | 5 | protected AggregateRoot(final ID id) { 6 | super(id); 7 | } 8 | } -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/delete/DeleteGenreUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.delete; 2 | 3 | import com.fullcycle.admin.catalogo.application.UnitUseCase; 4 | 5 | public abstract class DeleteGenreUseCase extends UnitUseCase { 6 | } 7 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/pagination/SearchQuery.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.pagination; 2 | 3 | public record SearchQuery( 4 | int page, 5 | int perPage, 6 | String terms, 7 | String sort, 8 | String direction 9 | ) { 10 | } 11 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/retrieve/get/GetGenreByIdUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.retrieve.get; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCase; 4 | 5 | public abstract class GetGenreByIdUseCase extends UseCase { 6 | } 7 | -------------------------------------------------------------------------------- /infrastructure/src/main/resources/application-production.yml: -------------------------------------------------------------------------------- 1 | mysql: 2 | username: ${DATABASE_MYSQL_USERNAME:root} 3 | password: ${DATABASE_MYSQL_PASSWORD:123456} 4 | schema: adm_videos 5 | url: ${DATABASE_MYSQL_URL:localhost:3306} 6 | 7 | spring: 8 | jpa: 9 | show-sql: false 10 | hibernate: 11 | ddl-auto: none -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/delete/DeleteCategoryUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.delete; 2 | 3 | import com.fullcycle.admin.catalogo.application.UnitUseCase; 4 | 5 | public abstract class DeleteCategoryUseCase 6 | extends UnitUseCase { 7 | } 8 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/create/CreateGenreUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.create; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCase; 4 | 5 | public abstract class CreateGenreUseCase 6 | extends UseCase { 7 | } 8 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/update/UpdateGenreUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.update; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCase; 4 | 5 | public abstract class UpdateGenreUseCase 6 | extends UseCase { 7 | } 8 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/retrieve/get/GetCategoryByIdUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.retrieve.get; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCase; 4 | 5 | public abstract class GetCategoryByIdUseCase 6 | extends UseCase { 7 | } 8 | -------------------------------------------------------------------------------- /infrastructure/src/main/resources/db/migration/V1__Initial.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE TABLE category ( 3 | id VARCHAR(36) NOT NULL PRIMARY KEY, 4 | name VARCHAR(255) NOT NULL, 5 | description VARCHAR(4000), 6 | active BOOLEAN NOT NULL DEFAULT TRUE, 7 | created_at DATETIME(6) NOT NULL, 8 | updated_at DATETIME(6) NOT NULL, 9 | deleted_at DATETIME(6) NULL 10 | ); -------------------------------------------------------------------------------- /domain/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.fullcycle.admin.catalogo.domain' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' 14 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' 15 | } 16 | 17 | test { 18 | useJUnitPlatform() 19 | } -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/configuration/WebServerConfig.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.configuration; 2 | 3 | import org.springframework.context.annotation.ComponentScan; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | @ComponentScan("com.fullcycle.admin.catalogo") 8 | public class WebServerConfig { 9 | } -------------------------------------------------------------------------------- /infrastructure/src/main/resources/application-test-integration.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | driver-class-name: org.h2.Driver 4 | username: root 5 | password: 123456 6 | url: jdbc:h2:mem:adm_videos_test;MODE=MYSQL;DATABASE_TO_LOWER=TRUE # É possível persistir em um arquivo alterando "mem:adm_videos_test" para "file:./.h2/dev" 7 | h2: 8 | console: 9 | enabled: true 10 | path: /h2 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | mysql: 5 | container_name: adm_videos_mysql 6 | image: mysql:latest 7 | environment: 8 | - MYSQL_ROOT_PASSWORD=123456 9 | - MYSQL_DATABASE=adm_videos 10 | security_opt: 11 | - seccomp:unconfined 12 | ports: 13 | - 3306:3306 14 | networks: 15 | - adm_videos_network 16 | 17 | networks: 18 | adm_videos_network: -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/utils/InstantUtils.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.utils; 2 | 3 | import java.time.Instant; 4 | import java.time.temporal.ChronoUnit; 5 | 6 | public final class InstantUtils { 7 | 8 | private InstantUtils() { 9 | } 10 | 11 | public static Instant now() { 12 | return Instant.now().truncatedTo(ChronoUnit.MICROS); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/update/UpdateGenreOutput.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.update; 2 | 3 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 4 | 5 | public record UpdateGenreOutput(String id) { 6 | 7 | public static UpdateGenreOutput from(final Genre aGenre) { 8 | return new UpdateGenreOutput(aGenre.getId().getValue()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/category/models/CreateCategoryRequest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.category.models; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public record CreateCategoryRequest( 6 | @JsonProperty("name") String name, 7 | @JsonProperty("description") String description, 8 | @JsonProperty("is_active") Boolean active 9 | ) { 10 | } 11 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/category/models/UpdateCategoryRequest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.category.models; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public record UpdateCategoryRequest( 6 | @JsonProperty("name") String name, 7 | @JsonProperty("description") String description, 8 | @JsonProperty("is_active") Boolean active 9 | ) { 10 | } 11 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/exceptions/NotificationException.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.exceptions; 2 | 3 | import com.fullcycle.admin.catalogo.domain.validation.handler.Notification; 4 | 5 | public class NotificationException extends DomainException { 6 | 7 | public NotificationException(final String aMessage, final Notification notification) { 8 | super(aMessage, notification.getErrors()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/exceptions/NoStacktraceException.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.exceptions; 2 | 3 | public class NoStacktraceException extends RuntimeException { 4 | 5 | public NoStacktraceException(final String message) { 6 | this(message, null); 7 | } 8 | 9 | public NoStacktraceException(final String message, final Throwable cause) { 10 | super(message, cause, true, false); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/create/CreateCategoryUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.create; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCase; 4 | import com.fullcycle.admin.catalogo.domain.validation.handler.Notification; 5 | import io.vavr.control.Either; 6 | 7 | public abstract class CreateCategoryUseCase 8 | extends UseCase> { 9 | } 10 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/update/UpdateCategoryUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.update; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCase; 4 | import com.fullcycle.admin.catalogo.domain.validation.handler.Notification; 5 | import io.vavr.control.Either; 6 | 7 | public abstract class UpdateCategoryUseCase 8 | extends UseCase> { 9 | } 10 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/retrieve/list/ListGenreUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.retrieve.list; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCase; 4 | import com.fullcycle.admin.catalogo.domain.pagination.Pagination; 5 | import com.fullcycle.admin.catalogo.domain.pagination.SearchQuery; 6 | 7 | public abstract class ListGenreUseCase 8 | extends UseCase> { 9 | } 10 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/validation/Validator.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.validation; 2 | 3 | public abstract class Validator { 4 | 5 | private final ValidationHandler handler; 6 | 7 | protected Validator(final ValidationHandler aHandler) { 8 | this.handler = aHandler; 9 | } 10 | 11 | public abstract void validate(); 12 | 13 | protected ValidationHandler validationHandler() { 14 | return this.handler; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/retrieve/list/ListCategoriesUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.retrieve.list; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCase; 4 | import com.fullcycle.admin.catalogo.domain.pagination.SearchQuery; 5 | import com.fullcycle.admin.catalogo.domain.pagination.Pagination; 6 | 7 | public abstract class ListCategoriesUseCase 8 | extends UseCase> { 9 | } 10 | -------------------------------------------------------------------------------- /application/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.fullcycle.admin.catalogo.application' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | implementation(project(":domain")) 14 | 15 | implementation 'io.vavr:vavr:0.10.4' 16 | 17 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' 18 | testImplementation 'org.mockito:mockito-junit-jupiter:4.5.1' 19 | 20 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' 21 | } 22 | 23 | test { 24 | useJUnitPlatform() 25 | } -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/create/CreateGenreOutput.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.create; 2 | 3 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 4 | 5 | public record CreateGenreOutput( 6 | String id 7 | ) { 8 | 9 | public static CreateGenreOutput from(final String anId) { 10 | return new CreateGenreOutput(anId); 11 | } 12 | 13 | public static CreateGenreOutput from(final Genre aGenre) { 14 | return new CreateGenreOutput(aGenre.getId().getValue()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/create/CreateCategoryCommand.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.create; 2 | 3 | public record CreateCategoryCommand( 4 | String name, 5 | String description, 6 | boolean isActive 7 | ) { 8 | 9 | public static CreateCategoryCommand with( 10 | final String aName, 11 | final String aDescription, 12 | final boolean isActive 13 | ) { 14 | return new CreateCategoryCommand(aName, aDescription, isActive); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/genre/GenreGateway.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.genre; 2 | 3 | import com.fullcycle.admin.catalogo.domain.pagination.Pagination; 4 | import com.fullcycle.admin.catalogo.domain.pagination.SearchQuery; 5 | 6 | import java.util.Optional; 7 | 8 | public interface GenreGateway { 9 | 10 | Genre create(Genre aGenre); 11 | 12 | void deleteById(GenreID anId); 13 | 14 | Optional findById(GenreID anId); 15 | 16 | Genre update(Genre aGenre); 17 | 18 | Pagination findAll(SearchQuery aQuery); 19 | } 20 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/genre/persistence/GenreRepository.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.genre.persistence; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.jpa.domain.Specification; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | public interface GenreRepository extends JpaRepository { 9 | 10 | Page findAll(Specification whereClause, Pageable page); 11 | } 12 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/configuration/ObjectMapperConfig.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.configuration; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fullcycle.admin.catalogo.infrastructure.configuration.json.Json; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class ObjectMapperConfig { 10 | 11 | @Bean 12 | public ObjectMapper objectMapper() { 13 | return Json.mapper(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/create/CreateCategoryOutput.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.create; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.Category; 4 | 5 | public record CreateCategoryOutput( 6 | String id 7 | ) { 8 | 9 | public static CreateCategoryOutput from(final String anId) { 10 | return new CreateCategoryOutput(anId); 11 | } 12 | 13 | public static CreateCategoryOutput from(final Category aCategory) { 14 | return new CreateCategoryOutput(aCategory.getId().getValue()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/update/UpdateCategoryOutput.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.update; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.Category; 4 | 5 | public record UpdateCategoryOutput( 6 | String id 7 | ) { 8 | 9 | public static UpdateCategoryOutput from(final String anId) { 10 | return new UpdateCategoryOutput(anId); 11 | } 12 | 13 | public static UpdateCategoryOutput from(final Category aCategory) { 14 | return new UpdateCategoryOutput(aCategory.getId().getValue()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | .history/ -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/create/CreateGenreCommand.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.create; 2 | 3 | import java.util.List; 4 | 5 | public record CreateGenreCommand( 6 | String name, 7 | boolean isActive, 8 | List categories 9 | ) { 10 | 11 | public static CreateGenreCommand with( 12 | final String aName, 13 | final Boolean isActive, 14 | final List categories 15 | ) { 16 | return new CreateGenreCommand(aName, isActive != null ? isActive : true, categories); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/utils/SpecificationUtils.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.utils; 2 | 3 | import org.springframework.data.jpa.domain.Specification; 4 | 5 | public final class SpecificationUtils { 6 | 7 | private SpecificationUtils() { 8 | } 9 | 10 | public static Specification like(final String prop, final String term) { 11 | return (root, query, cb) -> cb.like(cb.upper(root.get(prop)), like(term.toUpperCase())); 12 | } 13 | 14 | private static String like(final String term) { 15 | return "%" + term + "%"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/update/UpdateCategoryCommand.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.update; 2 | 3 | public record UpdateCategoryCommand( 4 | String id, 5 | String name, 6 | String description, 7 | boolean isActive 8 | ) { 9 | 10 | public static UpdateCategoryCommand with( 11 | final String anId, 12 | final String aName, 13 | final String aDescription, 14 | final boolean isActive 15 | ) { 16 | return new UpdateCategoryCommand(anId, aName, aDescription, isActive); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/pagination/Pagination.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.pagination; 2 | 3 | import java.util.List; 4 | import java.util.function.Function; 5 | 6 | public record Pagination( 7 | int currentPage, 8 | int perPage, 9 | long total, 10 | List items 11 | ) { 12 | 13 | public Pagination map(final Function mapper) { 14 | final List aNewList = this.items.stream() 15 | .map(mapper) 16 | .toList(); 17 | 18 | return new Pagination<>(currentPage(), perPage(), total(), aNewList); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/category/models/CategoryListResponse.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.category.models; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.time.Instant; 6 | 7 | public record CategoryListResponse( 8 | @JsonProperty("id") String id, 9 | @JsonProperty("name") String name, 10 | @JsonProperty("description") String description, 11 | @JsonProperty("is_active") Boolean active, 12 | @JsonProperty("created_at") Instant createdAt, 13 | @JsonProperty("deleted_at") Instant deletedAt 14 | ) { 15 | } 16 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/Main.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure; 2 | 3 | import com.fullcycle.admin.catalogo.infrastructure.configuration.WebServerConfig; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.core.env.AbstractEnvironment; 7 | 8 | @SpringBootApplication 9 | public class Main { 10 | 11 | public static void main(String[] args) { 12 | System.setProperty(AbstractEnvironment.DEFAULT_PROFILES_PROPERTY_NAME, "development"); 13 | SpringApplication.run(WebServerConfig.class, args); 14 | } 15 | } -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/update/UpdateGenreCommand.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.update; 2 | 3 | import java.util.List; 4 | 5 | public record UpdateGenreCommand( 6 | String id, 7 | String name, 8 | boolean isActive, 9 | List categories 10 | ) { 11 | 12 | public static UpdateGenreCommand with( 13 | final String id, 14 | final String name, 15 | final Boolean isActive, 16 | final List categories 17 | ) { 18 | return new UpdateGenreCommand(id, name, isActive != null ? isActive : true, categories); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/IntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo; 2 | 3 | import com.fullcycle.admin.catalogo.infrastructure.configuration.WebServerConfig; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.ActiveProfiles; 7 | 8 | import java.lang.annotation.*; 9 | 10 | @Target(ElementType.TYPE) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Inherited 13 | @ActiveProfiles("test-integration") 14 | @SpringBootTest(classes = WebServerConfig.class) 15 | @ExtendWith(MySQLCleanUpExtension.class) 16 | public @interface IntegrationTest { 17 | } 18 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/category/models/CategoryResponse.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.category.models; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.time.Instant; 6 | 7 | public record CategoryResponse( 8 | @JsonProperty("id") String id, 9 | @JsonProperty("name") String name, 10 | @JsonProperty("description") String description, 11 | @JsonProperty("is_active") Boolean active, 12 | @JsonProperty("created_at") Instant createdAt, 13 | @JsonProperty("updated_at") Instant updatedAt, 14 | @JsonProperty("deleted_at") Instant deletedAt 15 | ) { 16 | } 17 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/category/CategoryGateway.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.category; 2 | 3 | import com.fullcycle.admin.catalogo.domain.pagination.Pagination; 4 | import com.fullcycle.admin.catalogo.domain.pagination.SearchQuery; 5 | 6 | import java.util.List; 7 | import java.util.Optional; 8 | 9 | public interface CategoryGateway { 10 | 11 | Category create(Category aCategory); 12 | 13 | void deleteById(CategoryID anId); 14 | 15 | Optional findById(CategoryID anId); 16 | 17 | Category update(Category aCategory); 18 | 19 | Pagination findAll(SearchQuery aQuery); 20 | 21 | List existsByIds(Iterable ids); 22 | } 23 | -------------------------------------------------------------------------------- /infrastructure/src/main/resources/db/migration/V2__Create_Genre_Aggregate.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE TABLE genres ( 3 | id VARCHAR(36) NOT NULL PRIMARY KEY, 4 | name VARCHAR(255) NOT NULL, 5 | active BOOLEAN NOT NULL DEFAULT TRUE, 6 | created_at DATETIME(6) NOT NULL, 7 | updated_at DATETIME(6) NOT NULL, 8 | deleted_at DATETIME(6) NULL 9 | ); 10 | 11 | CREATE TABLE genres_categories ( 12 | genre_id VARCHAR(36) NOT NULL, 13 | category_id VARCHAR(36) NOT NULL, 14 | CONSTRAINT idx_genre_category UNIQUE (genre_id, category_id), 15 | CONSTRAINT fk_genre_id FOREIGN KEY (genre_id) REFERENCES genres (id) ON DELETE CASCADE, 16 | CONSTRAINT fk_category_id FOREIGN KEY (category_id) REFERENCES category (id) ON DELETE CASCADE 17 | ); -------------------------------------------------------------------------------- /application/src/test/java/com/fullcycle/admin/catalogo/application/UseCaseTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application; 2 | 3 | import org.junit.jupiter.api.extension.BeforeEachCallback; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.junit.jupiter.api.extension.ExtensionContext; 6 | import org.mockito.Mockito; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import java.util.List; 10 | 11 | @ExtendWith(MockitoExtension.class) 12 | public abstract class UseCaseTest implements BeforeEachCallback { 13 | 14 | @Override 15 | public void beforeEach(final ExtensionContext context) throws Exception { 16 | Mockito.reset(getMocks().toArray()); 17 | } 18 | 19 | protected abstract List getMocks(); 20 | } 21 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/delete/DefaultDeleteGenreUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.delete; 2 | 3 | import com.fullcycle.admin.catalogo.domain.genre.GenreGateway; 4 | import com.fullcycle.admin.catalogo.domain.genre.GenreID; 5 | 6 | import java.util.Objects; 7 | 8 | public class DefaultDeleteGenreUseCase extends DeleteGenreUseCase { 9 | 10 | private final GenreGateway genreGateway; 11 | 12 | public DefaultDeleteGenreUseCase(final GenreGateway genreGateway) { 13 | this.genreGateway = Objects.requireNonNull(genreGateway); 14 | } 15 | 16 | @Override 17 | public void execute(final String anIn) { 18 | this.genreGateway.deleteById(GenreID.from(anIn)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/E2ETest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo; 2 | 3 | import com.fullcycle.admin.catalogo.infrastructure.configuration.WebServerConfig; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.ActiveProfiles; 8 | 9 | import java.lang.annotation.*; 10 | 11 | @Target(ElementType.TYPE) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Inherited 14 | @ActiveProfiles("test-e2e") 15 | @SpringBootTest(classes = WebServerConfig.class) 16 | @ExtendWith(MySQLCleanUpExtension.class) 17 | @AutoConfigureMockMvc 18 | public @interface E2ETest { 19 | } 20 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/JacksonTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo; 2 | 3 | import com.fullcycle.admin.catalogo.infrastructure.configuration.ObjectMapperConfig; 4 | import org.springframework.boot.test.autoconfigure.json.JsonTest; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.context.annotation.FilterType; 7 | import org.springframework.test.context.ActiveProfiles; 8 | 9 | import java.lang.annotation.*; 10 | 11 | @Target(ElementType.TYPE) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Inherited 14 | @ActiveProfiles("test-integration") 15 | @JsonTest(includeFilters = { 16 | @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = ObjectMapperConfig.class) 17 | }) 18 | public @interface JacksonTest { 19 | } 20 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/delete/DefaultDeleteCategoryUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.delete; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 4 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 5 | 6 | import java.util.Objects; 7 | 8 | public class DefaultDeleteCategoryUseCase extends DeleteCategoryUseCase { 9 | 10 | private final CategoryGateway categoryGateway; 11 | 12 | public DefaultDeleteCategoryUseCase(final CategoryGateway categoryGateway) { 13 | this.categoryGateway = Objects.requireNonNull(categoryGateway); 14 | } 15 | 16 | @Override 17 | public void execute(final String anIn) { 18 | this.categoryGateway.deleteById(CategoryID.from(anIn)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/ControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo; 2 | 3 | import com.fullcycle.admin.catalogo.infrastructure.configuration.ObjectMapperConfig; 4 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 5 | import org.springframework.context.annotation.Import; 6 | import org.springframework.core.annotation.AliasFor; 7 | import org.springframework.test.context.ActiveProfiles; 8 | 9 | import java.lang.annotation.*; 10 | 11 | @Target(ElementType.TYPE) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Inherited 14 | @ActiveProfiles("test-integration") 15 | @WebMvcTest 16 | @Import(ObjectMapperConfig.class) 17 | public @interface ControllerTest { 18 | 19 | @AliasFor(annotation = WebMvcTest.class, attribute = "controllers") 20 | Class[] controllers() default {}; 21 | } 22 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/validation/ValidationHandler.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.validation; 2 | 3 | import java.util.List; 4 | 5 | public interface ValidationHandler { 6 | 7 | ValidationHandler append(Error anError); 8 | 9 | ValidationHandler append(ValidationHandler anHandler); 10 | 11 | T validate(Validation aValidation); 12 | 13 | List getErrors(); 14 | 15 | default boolean hasError() { 16 | return getErrors() != null && !getErrors().isEmpty(); 17 | } 18 | 19 | default Error firstError() { 20 | if (getErrors() != null && !getErrors().isEmpty()) { 21 | return getErrors().get(0); 22 | } else { 23 | return null; 24 | } 25 | } 26 | 27 | interface Validation { 28 | T validate(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/category/persistence/CategoryRepository.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.category.persistence; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.jpa.domain.Specification; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.Query; 8 | import org.springframework.data.repository.query.Param; 9 | 10 | import java.util.List; 11 | 12 | public interface CategoryRepository extends JpaRepository { 13 | 14 | Page findAll(Specification whereClause, Pageable page); 15 | 16 | @Query(value = "select c.id from Category c where c.id in :ids") 17 | List existsByIds(@Param("ids") List ids); 18 | } 19 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/exceptions/DomainException.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.exceptions; 2 | 3 | import com.fullcycle.admin.catalogo.domain.validation.Error; 4 | 5 | import java.util.List; 6 | 7 | public class DomainException extends NoStacktraceException { 8 | 9 | protected final List errors; 10 | 11 | protected DomainException(final String aMessage, final List anErrors) { 12 | super(aMessage); 13 | this.errors = anErrors; 14 | } 15 | 16 | public static DomainException with(final Error anErrors) { 17 | return new DomainException(anErrors.message(), List.of(anErrors)); 18 | } 19 | 20 | public static DomainException with(final List anErrors) { 21 | return new DomainException("", anErrors); 22 | } 23 | 24 | public List getErrors() { 25 | return errors; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/retrieve/list/DefaultListGenreUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.retrieve.list; 2 | 3 | import com.fullcycle.admin.catalogo.domain.genre.GenreGateway; 4 | import com.fullcycle.admin.catalogo.domain.pagination.Pagination; 5 | import com.fullcycle.admin.catalogo.domain.pagination.SearchQuery; 6 | 7 | import java.util.Objects; 8 | 9 | public class DefaultListGenreUseCase extends ListGenreUseCase { 10 | 11 | private final GenreGateway genreGateway; 12 | 13 | public DefaultListGenreUseCase(final GenreGateway genreGateway) { 14 | this.genreGateway = Objects.requireNonNull(genreGateway); 15 | } 16 | 17 | @Override 18 | public Pagination execute(final SearchQuery aQuery) { 19 | return this.genreGateway.findAll(aQuery) 20 | .map(GenreListOutput::from); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/MySQLGatewayTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo; 2 | 3 | import org.junit.jupiter.api.extension.ExtendWith; 4 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.context.annotation.FilterType; 7 | import org.springframework.test.context.ActiveProfiles; 8 | 9 | import java.lang.annotation.*; 10 | 11 | @Target(ElementType.TYPE) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Inherited 14 | @ActiveProfiles("test-integration") 15 | @ComponentScan( 16 | basePackages = "com.fullcycle.admin.catalogo", 17 | includeFilters = { 18 | @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".[MySQLGateway]") 19 | } 20 | ) 21 | @DataJpaTest 22 | @ExtendWith(MySQLCleanUpExtension.class) 23 | public @interface MySQLGatewayTest { 24 | } 25 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/retrieve/list/DefaultListCategoriesUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.retrieve.list; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 4 | import com.fullcycle.admin.catalogo.domain.pagination.SearchQuery; 5 | import com.fullcycle.admin.catalogo.domain.pagination.Pagination; 6 | 7 | import java.util.Objects; 8 | 9 | public class DefaultListCategoriesUseCase extends ListCategoriesUseCase { 10 | 11 | private final CategoryGateway categoryGateway; 12 | 13 | public DefaultListCategoriesUseCase(final CategoryGateway categoryGateway) { 14 | this.categoryGateway = Objects.requireNonNull(categoryGateway); 15 | } 16 | 17 | @Override 18 | public Pagination execute(final SearchQuery aQuery) { 19 | return this.categoryGateway.findAll(aQuery) 20 | .map(CategoryListOutput::from); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/retrieve/list/CategoryListOutput.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.retrieve.list; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.Category; 4 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 5 | 6 | import java.time.Instant; 7 | 8 | public record CategoryListOutput( 9 | CategoryID id, 10 | String name, 11 | String description, 12 | boolean isActive, 13 | Instant createdAt, 14 | Instant deletedAt 15 | ) { 16 | 17 | public static CategoryListOutput from(final Category aCategory) { 18 | return new CategoryListOutput( 19 | aCategory.getId(), 20 | aCategory.getName(), 21 | aCategory.getDescription(), 22 | aCategory.isActive(), 23 | aCategory.getCreatedAt(), 24 | aCategory.getDeletedAt() 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/retrieve/list/GenreListOutput.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.retrieve.list; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 4 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 5 | 6 | import java.time.Instant; 7 | import java.util.List; 8 | 9 | public record GenreListOutput( 10 | String name, 11 | boolean isActive, 12 | List categories, 13 | Instant createdAt, 14 | Instant deletedAt 15 | ) { 16 | 17 | public static GenreListOutput from(final Genre aGenre) { 18 | return new GenreListOutput( 19 | aGenre.getName(), 20 | aGenre.isActive(), 21 | aGenre.getCategories().stream() 22 | .map(CategoryID::getValue) 23 | .toList(), 24 | aGenre.getCreatedAt(), 25 | aGenre.getDeletedAt() 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/Entity.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain; 2 | 3 | import com.fullcycle.admin.catalogo.domain.validation.ValidationHandler; 4 | 5 | import java.util.Objects; 6 | 7 | public abstract class Entity { 8 | 9 | protected final ID id; 10 | 11 | protected Entity(final ID id) { 12 | Objects.requireNonNull(id, "'id' should not be null"); 13 | this.id = id; 14 | } 15 | 16 | public abstract void validate(ValidationHandler handler); 17 | 18 | public ID getId() { 19 | return id; 20 | } 21 | 22 | @Override 23 | public boolean equals(final Object o) { 24 | if (this == o) return true; 25 | if (o == null || getClass() != o.getClass()) return false; 26 | final Entity entity = (Entity) o; 27 | return getId().equals(entity.getId()); 28 | } 29 | 30 | @Override 31 | public int hashCode() { 32 | return Objects.hash(getId()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/exceptions/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.exceptions; 2 | 3 | import com.fullcycle.admin.catalogo.domain.AggregateRoot; 4 | import com.fullcycle.admin.catalogo.domain.Identifier; 5 | import com.fullcycle.admin.catalogo.domain.validation.Error; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | public class NotFoundException extends DomainException { 11 | 12 | protected NotFoundException(final String aMessage, final List anErrors) { 13 | super(aMessage, anErrors); 14 | } 15 | 16 | public static NotFoundException with( 17 | final Class> anAggregate, 18 | final Identifier id 19 | ) { 20 | final var anError = "%s with ID %s was not found".formatted( 21 | anAggregate.getSimpleName(), 22 | id.getValue() 23 | ); 24 | return new NotFoundException(anError, Collections.emptyList()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/retrieve/get/CategoryOutput.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.retrieve.get; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.Category; 4 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 5 | 6 | import java.time.Instant; 7 | 8 | public record CategoryOutput( 9 | CategoryID id, 10 | String name, 11 | String description, 12 | boolean isActive, 13 | Instant createdAt, 14 | Instant updatedAt, 15 | Instant deletedAt 16 | ) { 17 | 18 | public static CategoryOutput from(final Category aCategory) { 19 | return new CategoryOutput( 20 | aCategory.getId(), 21 | aCategory.getName(), 22 | aCategory.getDescription(), 23 | aCategory.isActive(), 24 | aCategory.getCreatedAt(), 25 | aCategory.getUpdatedAt(), 26 | aCategory.getDeletedAt() 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/retrieve/get/DefaultGetGenreByIdUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.retrieve.get; 2 | 3 | import com.fullcycle.admin.catalogo.domain.exceptions.NotFoundException; 4 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 5 | import com.fullcycle.admin.catalogo.domain.genre.GenreGateway; 6 | import com.fullcycle.admin.catalogo.domain.genre.GenreID; 7 | 8 | import java.util.Objects; 9 | 10 | public class DefaultGetGenreByIdUseCase extends GetGenreByIdUseCase{ 11 | 12 | private final GenreGateway genreGateway; 13 | 14 | public DefaultGetGenreByIdUseCase(final GenreGateway genreGateway) { 15 | this.genreGateway = Objects.requireNonNull(genreGateway); 16 | } 17 | 18 | @Override 19 | public GenreOutput execute(final String anIn) { 20 | final var aGenreId = GenreID.from(anIn); 21 | return this.genreGateway.findById(aGenreId) 22 | .map(GenreOutput::from) 23 | .orElseThrow(() -> NotFoundException.with(Genre.class, aGenreId)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/retrieve/get/GenreOutput.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.retrieve.get; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 4 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 5 | 6 | import java.time.Instant; 7 | import java.util.List; 8 | 9 | public record GenreOutput( 10 | String id, 11 | String name, 12 | boolean isActive, 13 | List categories, 14 | Instant createdAt, 15 | Instant updatedAt, 16 | Instant deletedAt 17 | ) { 18 | 19 | public static GenreOutput from(final Genre aGenre) { 20 | return new GenreOutput( 21 | aGenre.getId().getValue(), 22 | aGenre.getName(), 23 | aGenre.isActive(), 24 | aGenre.getCategories().stream() 25 | .map(CategoryID::getValue) 26 | .toList(), 27 | aGenre.getCreatedAt(), 28 | aGenre.getUpdatedAt(), 29 | aGenre.getDeletedAt() 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/validation/handler/ThrowsValidationHandler.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.validation.handler; 2 | 3 | import com.fullcycle.admin.catalogo.domain.exceptions.DomainException; 4 | import com.fullcycle.admin.catalogo.domain.validation.Error; 5 | import com.fullcycle.admin.catalogo.domain.validation.ValidationHandler; 6 | 7 | import java.util.List; 8 | 9 | public class ThrowsValidationHandler implements ValidationHandler { 10 | 11 | @Override 12 | public ValidationHandler append(final Error anError) { 13 | throw DomainException.with(anError); 14 | } 15 | 16 | @Override 17 | public ValidationHandler append(final ValidationHandler anHandler) { 18 | throw DomainException.with(anHandler.getErrors()); 19 | } 20 | 21 | @Override 22 | public T validate(final Validation aValidation) { 23 | try { 24 | return aValidation.validate(); 25 | } catch (final Exception ex) { 26 | throw DomainException.with(new Error(ex.getMessage())); 27 | } 28 | } 29 | 30 | @Override 31 | public List getErrors() { 32 | return List.of(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/MySQLCleanUpExtension.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo; 2 | 3 | import com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryRepository; 4 | import com.fullcycle.admin.catalogo.infrastructure.genre.persistence.GenreRepository; 5 | import java.util.Collection; 6 | import java.util.List; 7 | import org.junit.jupiter.api.extension.BeforeEachCallback; 8 | import org.junit.jupiter.api.extension.ExtensionContext; 9 | import org.springframework.data.repository.CrudRepository; 10 | import org.springframework.test.context.junit.jupiter.SpringExtension; 11 | 12 | public class MySQLCleanUpExtension implements BeforeEachCallback { 13 | 14 | @Override 15 | public void beforeEach(final ExtensionContext context) { 16 | final var appContext = SpringExtension.getApplicationContext(context); 17 | 18 | cleanUp(List.of( 19 | appContext.getBean(GenreRepository.class), 20 | appContext.getBean(CategoryRepository.class) 21 | )); 22 | } 23 | 24 | private void cleanUp(final Collection repositories) { 25 | repositories.forEach(CrudRepository::deleteAll); 26 | } 27 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/genre/GenreID.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.genre; 2 | 3 | import com.fullcycle.admin.catalogo.domain.Identifier; 4 | 5 | import java.util.Objects; 6 | import java.util.UUID; 7 | 8 | public class GenreID extends Identifier { 9 | 10 | private final String value; 11 | 12 | private GenreID(final String value) { 13 | Objects.requireNonNull(value); 14 | this.value = value; 15 | } 16 | 17 | public static GenreID unique() { 18 | return GenreID.from(UUID.randomUUID()); 19 | } 20 | 21 | public static GenreID from(final String anId) { 22 | return new GenreID(anId); 23 | } 24 | 25 | public static GenreID from(final UUID anId) { 26 | return new GenreID(anId.toString().toLowerCase()); 27 | } 28 | 29 | @Override 30 | public String getValue() { 31 | return value; 32 | } 33 | 34 | @Override 35 | public boolean equals(Object o) { 36 | if (this == o) return true; 37 | if (o == null || getClass() != o.getClass()) return false; 38 | final GenreID that = (GenreID) o; 39 | return getValue().equals(that.getValue()); 40 | } 41 | 42 | @Override 43 | public int hashCode() { 44 | return Objects.hash(getValue()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/retrieve/get/DefaultGetCategoryByIdUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.retrieve.get; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.Category; 4 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 5 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 6 | import com.fullcycle.admin.catalogo.domain.exceptions.NotFoundException; 7 | 8 | import java.util.Objects; 9 | import java.util.function.Supplier; 10 | 11 | public class DefaultGetCategoryByIdUseCase extends GetCategoryByIdUseCase { 12 | 13 | private final CategoryGateway categoryGateway; 14 | 15 | public DefaultGetCategoryByIdUseCase(final CategoryGateway categoryGateway) { 16 | this.categoryGateway = Objects.requireNonNull(categoryGateway); 17 | } 18 | 19 | @Override 20 | public CategoryOutput execute(final String anIn) { 21 | final var anCategoryID = CategoryID.from(anIn); 22 | 23 | return this.categoryGateway.findById(anCategoryID) 24 | .map(CategoryOutput::from) 25 | .orElseThrow(notFound(anCategoryID)); 26 | } 27 | 28 | private Supplier notFound(final CategoryID anId) { 29 | return () -> NotFoundException.with(Category.class, anId); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/category/CategoryID.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.category; 2 | 3 | import com.fullcycle.admin.catalogo.domain.Identifier; 4 | 5 | import java.util.Objects; 6 | import java.util.UUID; 7 | 8 | public class CategoryID extends Identifier { 9 | private final String value; 10 | 11 | private CategoryID(final String value) { 12 | Objects.requireNonNull(value); 13 | this.value = value; 14 | } 15 | 16 | public static CategoryID unique() { 17 | return CategoryID.from(UUID.randomUUID()); 18 | } 19 | 20 | public static CategoryID from(final String anId) { 21 | return new CategoryID(anId); 22 | } 23 | 24 | public static CategoryID from(final UUID anId) { 25 | return new CategoryID(anId.toString().toLowerCase()); 26 | } 27 | 28 | @Override 29 | public String getValue() { 30 | return value; 31 | } 32 | 33 | @Override 34 | public boolean equals(Object o) { 35 | if (this == o) return true; 36 | if (o == null || getClass() != o.getClass()) return false; 37 | final CategoryID that = (CategoryID) o; 38 | return getValue().equals(that.getValue()); 39 | } 40 | 41 | @Override 42 | public int hashCode() { 43 | return Objects.hash(getValue()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/category/presenters/CategoryApiPresenter.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.category.presenters; 2 | 3 | import com.fullcycle.admin.catalogo.application.category.retrieve.get.CategoryOutput; 4 | import com.fullcycle.admin.catalogo.application.category.retrieve.list.CategoryListOutput; 5 | import com.fullcycle.admin.catalogo.infrastructure.category.models.CategoryResponse; 6 | import com.fullcycle.admin.catalogo.infrastructure.category.models.CategoryListResponse; 7 | 8 | public interface CategoryApiPresenter { 9 | 10 | static CategoryResponse present(final CategoryOutput output) { 11 | return new CategoryResponse( 12 | output.id().getValue(), 13 | output.name(), 14 | output.description(), 15 | output.isActive(), 16 | output.createdAt(), 17 | output.updatedAt(), 18 | output.deletedAt() 19 | ); 20 | } 21 | 22 | static CategoryListResponse present(final CategoryListOutput output) { 23 | return new CategoryListResponse( 24 | output.id().getValue(), 25 | output.name(), 26 | output.description(), 27 | output.isActive(), 28 | output.createdAt(), 29 | output.deletedAt() 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/infrastructure/category/models/UpdateCategoryRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.category.models; 2 | 3 | import com.fullcycle.admin.catalogo.JacksonTest; 4 | import org.assertj.core.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.json.JacksonTester; 8 | 9 | @JacksonTest 10 | public class UpdateCategoryRequestTest { 11 | 12 | @Autowired 13 | private JacksonTester json; 14 | 15 | @Test 16 | public void testUnmarshall() throws Exception { 17 | final var expectedName = "Filmes"; 18 | final var expectedDescription = "A categoria mais assistida"; 19 | final var expectedIsActive = true; 20 | 21 | final var json = """ 22 | { 23 | "name": "%s", 24 | "description": "%s", 25 | "is_active": %s 26 | } 27 | """.formatted(expectedName, expectedDescription, expectedIsActive); 28 | 29 | final var actualJson = this.json.parse(json); 30 | 31 | Assertions.assertThat(actualJson) 32 | .hasFieldOrPropertyWithValue("name", expectedName) 33 | .hasFieldOrPropertyWithValue("description", expectedDescription) 34 | .hasFieldOrPropertyWithValue("active", expectedIsActive); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/api/controllers/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.api.controllers; 2 | 3 | import com.fullcycle.admin.catalogo.domain.exceptions.DomainException; 4 | import com.fullcycle.admin.catalogo.domain.exceptions.NotFoundException; 5 | import com.fullcycle.admin.catalogo.domain.validation.Error; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.bind.annotation.RestControllerAdvice; 10 | 11 | import java.util.List; 12 | 13 | @RestControllerAdvice 14 | public class GlobalExceptionHandler { 15 | 16 | @ExceptionHandler(value = NotFoundException.class) 17 | public ResponseEntity handleNotFoundException(final NotFoundException ex) { 18 | return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiError.from(ex)); 19 | } 20 | 21 | @ExceptionHandler(value = DomainException.class) 22 | public ResponseEntity handleDomainException(final DomainException ex) { 23 | return ResponseEntity.unprocessableEntity().body(ApiError.from(ex)); 24 | } 25 | 26 | record ApiError(String message, List errors) { 27 | static ApiError from(final DomainException ex) { 28 | return new ApiError(ex.getMessage(), ex.getErrors()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/genre/GenreValidator.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.genre; 2 | 3 | import com.fullcycle.admin.catalogo.domain.validation.Error; 4 | import com.fullcycle.admin.catalogo.domain.validation.ValidationHandler; 5 | import com.fullcycle.admin.catalogo.domain.validation.Validator; 6 | 7 | public class GenreValidator extends Validator { 8 | 9 | public static final int NAME_MAX_LENGTH = 255; 10 | public static final int NAME_MIN_LENGTH = 1; 11 | 12 | private final Genre genre; 13 | 14 | protected GenreValidator(final Genre aGenre, final ValidationHandler aHandler) { 15 | super(aHandler); 16 | this.genre = aGenre; 17 | } 18 | 19 | @Override 20 | public void validate() { 21 | checkNameConstraints(); 22 | } 23 | 24 | private void checkNameConstraints() { 25 | final var name = this.genre.getName(); 26 | if (name == null) { 27 | this.validationHandler().append(new Error("'name' should not be null")); 28 | return; 29 | } 30 | 31 | if (name.isBlank()) { 32 | this.validationHandler().append(new Error("'name' should not be empty")); 33 | return; 34 | } 35 | 36 | final int length = name.trim().length(); 37 | if (length > NAME_MAX_LENGTH || length < NAME_MIN_LENGTH) { 38 | this.validationHandler().append(new Error("'name' must be between 1 and 255 characters")); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/category/CategoryValidator.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.category; 2 | 3 | import com.fullcycle.admin.catalogo.domain.validation.Error; 4 | import com.fullcycle.admin.catalogo.domain.validation.ValidationHandler; 5 | import com.fullcycle.admin.catalogo.domain.validation.Validator; 6 | 7 | public class CategoryValidator extends Validator { 8 | 9 | public static final int NAME_MAX_LENGTH = 255; 10 | public static final int NAME_MIN_LENGTH = 3; 11 | private final Category category; 12 | 13 | public CategoryValidator(final Category aCategory, final ValidationHandler aHandler) { 14 | super(aHandler); 15 | this.category = aCategory; 16 | } 17 | 18 | @Override 19 | public void validate() { 20 | checkNameConstraints(); 21 | } 22 | 23 | private void checkNameConstraints() { 24 | final var name = this.category.getName(); 25 | if (name == null) { 26 | this.validationHandler().append(new Error("'name' should not be null")); 27 | return; 28 | } 29 | 30 | if (name.isBlank()) { 31 | this.validationHandler().append(new Error("'name' should not be empty")); 32 | return; 33 | } 34 | 35 | final int length = name.trim().length(); 36 | if (length > NAME_MAX_LENGTH || length < NAME_MIN_LENGTH) { 37 | this.validationHandler().append(new Error("'name' must be between 3 and 255 characters")); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/create/DefaultCreateCategoryUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.create; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.Category; 4 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 5 | import com.fullcycle.admin.catalogo.domain.validation.handler.Notification; 6 | import io.vavr.control.Either; 7 | 8 | import java.util.Objects; 9 | 10 | import static io.vavr.API.Left; 11 | import static io.vavr.API.Try; 12 | 13 | public class DefaultCreateCategoryUseCase extends CreateCategoryUseCase { 14 | 15 | private final CategoryGateway categoryGateway; 16 | 17 | public DefaultCreateCategoryUseCase(final CategoryGateway categoryGateway) { 18 | this.categoryGateway = Objects.requireNonNull(categoryGateway); 19 | } 20 | 21 | @Override 22 | public Either execute(final CreateCategoryCommand aCommand) { 23 | final var aName = aCommand.name(); 24 | final var aDescription = aCommand.description(); 25 | final var isActive = aCommand.isActive(); 26 | 27 | final var notification = Notification.create(); 28 | 29 | final var aCategory = Category.newCategory(aName, aDescription, isActive); 30 | aCategory.validate(notification); 31 | 32 | return notification.hasError() ? Left(notification) : create(aCategory); 33 | } 34 | 35 | private Either create(final Category aCategory) { 36 | return Try(() -> this.categoryGateway.create(aCategory)) 37 | .toEither() 38 | .bimap(Notification::create, CreateCategoryOutput::from); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/infrastructure/category/models/CategoryListResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.category.models; 2 | 3 | import com.fullcycle.admin.catalogo.JacksonTest; 4 | import org.assertj.core.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.json.JacksonTester; 8 | 9 | import java.time.Instant; 10 | 11 | @JacksonTest 12 | class CategoryListResponseTest { 13 | 14 | @Autowired 15 | private JacksonTester json; 16 | 17 | @Test 18 | public void testMarshall() throws Exception { 19 | final var expectedId = "123"; 20 | final var expectedName = "Filmes"; 21 | final var expectedDescription = "A categoria mais assistida"; 22 | final var expectedIsActive = false; 23 | final var expectedCreatedAt = Instant.now(); 24 | final var expectedDeletedAt = Instant.now(); 25 | 26 | final var response = new CategoryListResponse( 27 | expectedId, 28 | expectedName, 29 | expectedDescription, 30 | expectedIsActive, 31 | expectedCreatedAt, 32 | expectedDeletedAt 33 | ); 34 | 35 | final var actualJson = this.json.write(response); 36 | 37 | Assertions.assertThat(actualJson) 38 | .hasJsonPathValue("$.id", expectedId) 39 | .hasJsonPathValue("$.name", expectedName) 40 | .hasJsonPathValue("$.description", expectedDescription) 41 | .hasJsonPathValue("$.is_active", expectedIsActive) 42 | .hasJsonPathValue("$.created_at", expectedCreatedAt.toString()) 43 | .hasJsonPathValue("$.deleted_at", expectedDeletedAt.toString()); 44 | } 45 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/validation/handler/Notification.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.validation.handler; 2 | 3 | import com.fullcycle.admin.catalogo.domain.exceptions.DomainException; 4 | import com.fullcycle.admin.catalogo.domain.validation.Error; 5 | import com.fullcycle.admin.catalogo.domain.validation.ValidationHandler; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class Notification implements ValidationHandler { 11 | 12 | private final List errors; 13 | 14 | private Notification(final List errors) { 15 | this.errors = errors; 16 | } 17 | 18 | public static Notification create() { 19 | return new Notification(new ArrayList<>()); 20 | } 21 | 22 | public static Notification create(final Throwable t) { 23 | return create(new Error(t.getMessage())); 24 | } 25 | 26 | public static Notification create(final Error anError) { 27 | return new Notification(new ArrayList<>()).append(anError); 28 | } 29 | 30 | @Override 31 | public Notification append(final Error anError) { 32 | this.errors.add(anError); 33 | return this; 34 | } 35 | 36 | @Override 37 | public Notification append(final ValidationHandler anHandler) { 38 | this.errors.addAll(anHandler.getErrors()); 39 | return this; 40 | } 41 | 42 | @Override 43 | public T validate(final Validation aValidation) { 44 | try { 45 | return aValidation.validate(); 46 | } catch (final DomainException ex) { 47 | this.errors.addAll(ex.getErrors()); 48 | } catch (final Throwable t) { 49 | this.errors.add(new Error(t.getMessage())); 50 | } 51 | return null; 52 | } 53 | 54 | @Override 55 | public List getErrors() { 56 | return this.errors; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/genre/persistence/GenreCategoryJpaEntity.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.genre.persistence; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 4 | 5 | import javax.persistence.*; 6 | import java.util.Objects; 7 | 8 | @Entity 9 | @Table(name = "genres_categories") 10 | public class GenreCategoryJpaEntity { 11 | 12 | @EmbeddedId 13 | private GenreCategoryID id; 14 | 15 | @ManyToOne 16 | @MapsId("genreId") 17 | private GenreJpaEntity genre; 18 | 19 | public GenreCategoryJpaEntity() {} 20 | 21 | private GenreCategoryJpaEntity(final GenreJpaEntity aGenre, final CategoryID aCategoryId) { 22 | this.id = GenreCategoryID.from(aGenre.getId(), aCategoryId.getValue()); 23 | this.genre = aGenre; 24 | } 25 | 26 | public static GenreCategoryJpaEntity from(final GenreJpaEntity aGenre, final CategoryID aCategoryId) { 27 | return new GenreCategoryJpaEntity(aGenre, aCategoryId); 28 | } 29 | 30 | @Override 31 | public boolean equals(final Object o) { 32 | if (this == o) return true; 33 | if (o == null || getClass() != o.getClass()) return false; 34 | final GenreCategoryJpaEntity that = (GenreCategoryJpaEntity) o; 35 | return Objects.equals(getId(), that.getId()); 36 | } 37 | 38 | @Override 39 | public int hashCode() { 40 | return Objects.hash(getId()); 41 | } 42 | 43 | public GenreCategoryID getId() { 44 | return id; 45 | } 46 | 47 | public GenreCategoryJpaEntity setId(GenreCategoryID id) { 48 | this.id = id; 49 | return this; 50 | } 51 | 52 | public GenreJpaEntity getGenre() { 53 | return genre; 54 | } 55 | 56 | public GenreCategoryJpaEntity setGenre(GenreJpaEntity genre) { 57 | this.genre = genre; 58 | return this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/genre/persistence/GenreCategoryID.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.genre.persistence; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Embeddable; 5 | import java.io.Serializable; 6 | import java.util.Objects; 7 | 8 | @Embeddable 9 | public class GenreCategoryID implements Serializable { 10 | 11 | @Column(name = "genre_id", nullable = false) 12 | private String genreId; 13 | 14 | @Column(name = "category_id", nullable = false) 15 | private String categoryId; 16 | 17 | public GenreCategoryID() {} 18 | 19 | private GenreCategoryID(final String aGenreId, final String aCategoryId) { 20 | this.genreId = aGenreId; 21 | this.categoryId = aCategoryId; 22 | } 23 | 24 | public static GenreCategoryID from(final String aGenreId, final String aCategoryId) { 25 | return new GenreCategoryID(aGenreId, aCategoryId); 26 | } 27 | 28 | @Override 29 | public boolean equals(final Object o) { 30 | if (this == o) return true; 31 | if (o == null || getClass() != o.getClass()) return false; 32 | final GenreCategoryID that = (GenreCategoryID) o; 33 | return Objects.equals(getGenreId(), that.getGenreId()) && Objects.equals(getCategoryId(), that.getCategoryId()); 34 | } 35 | 36 | @Override 37 | public int hashCode() { 38 | return Objects.hash(getGenreId(), getCategoryId()); 39 | } 40 | 41 | public String getGenreId() { 42 | return genreId; 43 | } 44 | 45 | public GenreCategoryID setGenreId(String genreId) { 46 | this.genreId = genreId; 47 | return this; 48 | } 49 | 50 | public String getCategoryId() { 51 | return categoryId; 52 | } 53 | 54 | public GenreCategoryID setCategoryId(String categoryId) { 55 | this.categoryId = categoryId; 56 | return this; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/application/genre/delete/DeleteGenreUseCaseIT.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.delete; 2 | 3 | import com.fullcycle.admin.catalogo.IntegrationTest; 4 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 5 | import com.fullcycle.admin.catalogo.domain.genre.GenreGateway; 6 | import com.fullcycle.admin.catalogo.domain.genre.GenreID; 7 | import com.fullcycle.admin.catalogo.infrastructure.genre.persistence.GenreRepository; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | 12 | @IntegrationTest 13 | public class DeleteGenreUseCaseIT { 14 | 15 | @Autowired 16 | private DeleteGenreUseCase useCase; 17 | 18 | @Autowired 19 | private GenreGateway genreGateway; 20 | 21 | @Autowired 22 | private GenreRepository genreRepository; 23 | 24 | @Test 25 | public void givenAValidGenreId_whenCallsDeleteGenre_shouldDeleteGenre() { 26 | // given 27 | final var aGenre = genreGateway.create(Genre.newGenre("Ação", true)); 28 | 29 | final var expectedId = aGenre.getId(); 30 | 31 | Assertions.assertEquals(1, genreRepository.count()); 32 | 33 | // when 34 | Assertions.assertDoesNotThrow(() -> useCase.execute(expectedId.getValue())); 35 | 36 | // when 37 | Assertions.assertEquals(0, genreRepository.count()); 38 | } 39 | 40 | @Test 41 | public void givenAnInvalidGenreId_whenCallsDeleteGenre_shouldBeOk() { 42 | // given 43 | genreGateway.create(Genre.newGenre("Ação", true)); 44 | 45 | final var expectedId = GenreID.from("123"); 46 | 47 | Assertions.assertEquals(1, genreRepository.count()); 48 | 49 | // when 50 | Assertions.assertDoesNotThrow(() -> useCase.execute(expectedId.getValue())); 51 | 52 | // when 53 | Assertions.assertEquals(1, genreRepository.count()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /infrastructure/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath 'org.flywaydb:flyway-mysql:8.5.10' 7 | } 8 | } 9 | 10 | plugins { 11 | id 'java' 12 | id 'application' 13 | id 'org.springframework.boot' version '2.6.7' 14 | id 'io.spring.dependency-management' version '1.0.11.RELEASE' 15 | id 'org.flywaydb.flyway' version '8.5.10' 16 | } 17 | 18 | group 'com.fullcycle.admin.catalogo.infrastructure' 19 | version '1.0-SNAPSHOT' 20 | 21 | bootJar { 22 | archiveName 'application.jar' 23 | destinationDirectory.set(file("${rootProject.buildDir}/libs")) 24 | } 25 | 26 | repositories { 27 | mavenCentral() 28 | } 29 | 30 | dependencies { 31 | implementation(project(":domain")) 32 | implementation(project(":application")) 33 | 34 | implementation('io.vavr:vavr:0.10.4') 35 | 36 | implementation('mysql:mysql-connector-java') 37 | 38 | implementation('org.springdoc:springdoc-openapi-webmvc-core:1.6.8') 39 | implementation('org.springdoc:springdoc-openapi-ui:1.6.8') 40 | 41 | implementation('org.springframework.boot:spring-boot-starter-web') { 42 | exclude module: 'spring-boot-starter-tomcat' 43 | } 44 | implementation('org.springframework.boot:spring-boot-starter-undertow') 45 | implementation('org.springframework.boot:spring-boot-starter-data-jpa') 46 | 47 | implementation('com.fasterxml.jackson.module:jackson-module-afterburner') 48 | 49 | testImplementation('org.flywaydb:flyway-core') 50 | testImplementation('org.springframework.boot:spring-boot-starter-test') 51 | 52 | testImplementation('org.testcontainers:testcontainers:1.17.2') 53 | testImplementation('org.testcontainers:mysql:1.17.2') 54 | testImplementation('org.testcontainers:junit-jupiter:1.17.2') 55 | 56 | testRuntimeOnly('com.h2database:h2') 57 | } 58 | 59 | flyway { 60 | url = System.getenv('FLYWAY_DB') ?: 'jdbc:mysql://localhost:3306/adm_videos' 61 | user = System.getenv('FLYWAY_USER') ?: 'root' 62 | password = System.getenv('FLYWAY_PASS') ?: '123456' 63 | } 64 | 65 | test { 66 | useJUnitPlatform() 67 | } -------------------------------------------------------------------------------- /infrastructure/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | servlet: 4 | context-path: /api 5 | compression: 6 | enabled: true # Whether response compression is enabled. 7 | mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json # Comma-separated list of MIME types that should be compressed. 8 | min-response-size: 1024 # Minimum "Content-Length" value that is required for compression to be performed. 9 | undertow: 10 | threads: 11 | worker: 64 # Generally this should be reasonably high, at least 10 per CPU core: https://undertow.io/undertow-docs/undertow-docs-2.1.0/index.html#listeners-2 12 | io: 4 # One IO thread per CPU core is a reasonable default: https://undertow.io/undertow-docs/undertow-docs-2.1.0/index.html#listeners-2 13 | 14 | spring: 15 | datasource: 16 | url: jdbc:mysql://${mysql.url}/${mysql.schema}?useSSL=true&serverTimezone=UTC&characterEncoding=UTF-8 17 | username: ${mysql.username} 18 | password: ${mysql.password} 19 | hikari: 20 | auto-commit: false 21 | connection-timeout: 250 # É uma configuração em milliseconds. O ideal é manter baixo para que estoure timeout logo e não prenda as threads. 22 | max-lifetime: 600000 # Tempo máximo que uma conexão pode ficar aberta (10 min) - security. 23 | maximum-pool-size: 20 # Mantemos até no máx 20 conexões com o banco de dados. O ideal é manter baixo mesmo, pois é algo custoso para o banco gerenciar. https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing 24 | minimum-idle: 10 25 | pool-name: master 26 | jpa: 27 | open-in-view: false 28 | show-sql: true 29 | hibernate: 30 | ddl-auto: none 31 | properties: 32 | "[hibernate.dialect]": org.hibernate.dialect.MySQL5InnoDBDialect 33 | "[hibernate.generate_statistics]": false 34 | "[hibernate.connection.provider_disables_autocommit]": true 35 | # Para aumentar a performance ao máximo, desabilitamos o auto-commit e o open-in-view. 36 | # https://vladmihalcea.com/why-you-should-always-use-hibernate-connection-provider_disables_autocommit-for-resource-local-jpa-transactions/ -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/infrastructure/category/models/CreateCategoryRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.category.models; 2 | 3 | import com.fullcycle.admin.catalogo.JacksonTest; 4 | import org.assertj.core.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.json.JacksonTester; 8 | 9 | @JacksonTest 10 | class CreateCategoryRequestTest { 11 | 12 | @Autowired 13 | private JacksonTester json; 14 | 15 | @Test 16 | public void testMarshall() throws Exception { 17 | final var expectedName = "Filmes"; 18 | final var expectedDescription = "A categoria mais assistida"; 19 | final var expectedIsActive = true; 20 | 21 | final var request = 22 | new CreateCategoryRequest(expectedName, expectedDescription, expectedIsActive); 23 | 24 | final var actualJson = this.json.write(request); 25 | 26 | Assertions.assertThat(actualJson) 27 | .hasJsonPathValue("$.name", expectedName) 28 | .hasJsonPathValue("$.description", expectedDescription) 29 | .hasJsonPathValue("$.is_active", expectedIsActive); 30 | } 31 | 32 | @Test 33 | public void testUnmarshall() throws Exception { 34 | final var expectedName = "Filmes"; 35 | final var expectedDescription = "A categoria mais assistida"; 36 | final var expectedIsActive = true; 37 | 38 | final var json = """ 39 | { 40 | "name": "%s", 41 | "description": "%s", 42 | "is_active": %s 43 | } 44 | """.formatted(expectedName, expectedDescription, expectedIsActive); 45 | 46 | final var actualJson = this.json.parse(json); 47 | 48 | Assertions.assertThat(actualJson) 49 | .hasFieldOrPropertyWithValue("name", expectedName) 50 | .hasFieldOrPropertyWithValue("description", expectedDescription) 51 | .hasFieldOrPropertyWithValue("active", expectedIsActive); 52 | } 53 | } -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/category/update/DefaultUpdateCategoryUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.update; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.Category; 4 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 5 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 6 | import com.fullcycle.admin.catalogo.domain.exceptions.DomainException; 7 | import com.fullcycle.admin.catalogo.domain.exceptions.NotFoundException; 8 | import com.fullcycle.admin.catalogo.domain.validation.handler.Notification; 9 | import io.vavr.control.Either; 10 | 11 | import java.util.Objects; 12 | import java.util.function.Supplier; 13 | 14 | import static io.vavr.API.Left; 15 | import static io.vavr.API.Try; 16 | 17 | public class DefaultUpdateCategoryUseCase extends UpdateCategoryUseCase { 18 | 19 | private final CategoryGateway categoryGateway; 20 | 21 | public DefaultUpdateCategoryUseCase(final CategoryGateway categoryGateway) { 22 | this.categoryGateway = Objects.requireNonNull(categoryGateway); 23 | } 24 | 25 | @Override 26 | public Either execute(final UpdateCategoryCommand aCommand) { 27 | final var anId = CategoryID.from(aCommand.id()); 28 | final var aName = aCommand.name(); 29 | final var aDescription = aCommand.description(); 30 | final var isActive = aCommand.isActive(); 31 | 32 | final var aCategory = this.categoryGateway.findById(anId) 33 | .orElseThrow(notFound(anId)); 34 | 35 | final var notification = Notification.create(); 36 | aCategory 37 | .update(aName, aDescription, isActive) 38 | .validate(notification); 39 | 40 | return notification.hasError() ? Left(notification) : update(aCategory); 41 | } 42 | 43 | private Either update(final Category aCategory) { 44 | return Try(() -> this.categoryGateway.update(aCategory)) 45 | .toEither() 46 | .bimap(Notification::create, UpdateCategoryOutput::from); 47 | } 48 | 49 | private Supplier notFound(final CategoryID anId) { 50 | return () -> NotFoundException.with(Category.class, anId); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/configuration/usecases/CategoryUseCaseConfig.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.configuration.usecases; 2 | 3 | import com.fullcycle.admin.catalogo.application.category.create.CreateCategoryUseCase; 4 | import com.fullcycle.admin.catalogo.application.category.create.DefaultCreateCategoryUseCase; 5 | import com.fullcycle.admin.catalogo.application.category.delete.DefaultDeleteCategoryUseCase; 6 | import com.fullcycle.admin.catalogo.application.category.delete.DeleteCategoryUseCase; 7 | import com.fullcycle.admin.catalogo.application.category.retrieve.get.DefaultGetCategoryByIdUseCase; 8 | import com.fullcycle.admin.catalogo.application.category.retrieve.get.GetCategoryByIdUseCase; 9 | import com.fullcycle.admin.catalogo.application.category.retrieve.list.DefaultListCategoriesUseCase; 10 | import com.fullcycle.admin.catalogo.application.category.retrieve.list.ListCategoriesUseCase; 11 | import com.fullcycle.admin.catalogo.application.category.update.DefaultUpdateCategoryUseCase; 12 | import com.fullcycle.admin.catalogo.application.category.update.UpdateCategoryUseCase; 13 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.context.annotation.Configuration; 16 | 17 | @Configuration 18 | public class CategoryUseCaseConfig { 19 | 20 | private final CategoryGateway categoryGateway; 21 | 22 | public CategoryUseCaseConfig(final CategoryGateway categoryGateway) { 23 | this.categoryGateway = categoryGateway; 24 | } 25 | 26 | @Bean 27 | public CreateCategoryUseCase createCategoryUseCase() { 28 | return new DefaultCreateCategoryUseCase(categoryGateway); 29 | } 30 | 31 | @Bean 32 | public UpdateCategoryUseCase updateCategoryUseCase() { 33 | return new DefaultUpdateCategoryUseCase(categoryGateway); 34 | } 35 | 36 | @Bean 37 | public GetCategoryByIdUseCase getCategoryByIdUseCase() { 38 | return new DefaultGetCategoryByIdUseCase(categoryGateway); 39 | } 40 | 41 | @Bean 42 | public ListCategoriesUseCase listCategoriesUseCase() { 43 | return new DefaultListCategoriesUseCase(categoryGateway); 44 | } 45 | 46 | @Bean 47 | public DeleteCategoryUseCase deleteCategoryUseCase() { 48 | return new DefaultDeleteCategoryUseCase(categoryGateway); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/configuration/json/Json.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.configuration.json; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 6 | import com.fasterxml.jackson.databind.SerializationFeature; 7 | import com.fasterxml.jackson.databind.util.StdDateFormat; 8 | import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; 9 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 10 | import com.fasterxml.jackson.module.afterburner.AfterburnerModule; 11 | import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 12 | 13 | import java.util.concurrent.Callable; 14 | 15 | public enum Json { 16 | INSTANCE; 17 | 18 | public static ObjectMapper mapper() { 19 | return INSTANCE.mapper.copy(); 20 | } 21 | 22 | public static String writeValueAsString(final Object obj) { 23 | return invoke(() -> INSTANCE.mapper.writeValueAsString(obj)); 24 | } 25 | 26 | public static T readValue(final String json, final Class clazz) { 27 | return invoke(() -> INSTANCE.mapper.readValue(json, clazz)); 28 | } 29 | 30 | private final ObjectMapper mapper = new Jackson2ObjectMapperBuilder() 31 | .dateFormat(new StdDateFormat()) 32 | .featuresToDisable( 33 | DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 34 | DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, 35 | DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES, 36 | SerializationFeature.WRITE_DATES_AS_TIMESTAMPS 37 | ) 38 | .modules(new JavaTimeModule(), new Jdk8Module(), afterburnerModule()) 39 | .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) 40 | .build(); 41 | 42 | private AfterburnerModule afterburnerModule() { 43 | var module = new AfterburnerModule(); 44 | // make Afterburner generate bytecode only for public getters/setter and fields 45 | // without this, Java 9+ complains of "Illegal reflective access" 46 | module.setUseValueClassLoader(false); 47 | return module; 48 | } 49 | 50 | private static T invoke(final Callable callable) { 51 | try { 52 | return callable.call(); 53 | } catch (final Exception e) { 54 | throw new RuntimeException(e); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/configuration/usecases/GenreUseCaseConfig.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.configuration.usecases; 2 | 3 | import com.fullcycle.admin.catalogo.application.genre.create.CreateGenreUseCase; 4 | import com.fullcycle.admin.catalogo.application.genre.create.DefaultCreateGenreUseCase; 5 | import com.fullcycle.admin.catalogo.application.genre.delete.DefaultDeleteGenreUseCase; 6 | import com.fullcycle.admin.catalogo.application.genre.delete.DeleteGenreUseCase; 7 | import com.fullcycle.admin.catalogo.application.genre.retrieve.get.DefaultGetGenreByIdUseCase; 8 | import com.fullcycle.admin.catalogo.application.genre.retrieve.get.GetGenreByIdUseCase; 9 | import com.fullcycle.admin.catalogo.application.genre.retrieve.list.DefaultListGenreUseCase; 10 | import com.fullcycle.admin.catalogo.application.genre.retrieve.list.ListGenreUseCase; 11 | import com.fullcycle.admin.catalogo.application.genre.update.DefaultUpdateGenreUseCase; 12 | import com.fullcycle.admin.catalogo.application.genre.update.UpdateGenreUseCase; 13 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 14 | import com.fullcycle.admin.catalogo.domain.genre.GenreGateway; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.context.annotation.Configuration; 17 | 18 | import java.util.Objects; 19 | 20 | @Configuration 21 | public class GenreUseCaseConfig { 22 | 23 | private final CategoryGateway categoryGateway; 24 | private final GenreGateway genreGateway; 25 | 26 | public GenreUseCaseConfig( 27 | final CategoryGateway categoryGateway, 28 | final GenreGateway genreGateway 29 | ) { 30 | this.categoryGateway = Objects.requireNonNull(categoryGateway); 31 | this.genreGateway = Objects.requireNonNull(genreGateway); 32 | } 33 | 34 | @Bean 35 | public CreateGenreUseCase createGenreUseCase() { 36 | return new DefaultCreateGenreUseCase(categoryGateway, genreGateway); 37 | } 38 | 39 | @Bean 40 | public DeleteGenreUseCase deleteGenreUseCase() { 41 | return new DefaultDeleteGenreUseCase(genreGateway); 42 | } 43 | 44 | @Bean 45 | public GetGenreByIdUseCase getGenreByIdUseCase() { 46 | return new DefaultGetGenreByIdUseCase(genreGateway); 47 | } 48 | 49 | @Bean 50 | public ListGenreUseCase listGenreUseCase() { 51 | return new DefaultListGenreUseCase(genreGateway); 52 | } 53 | 54 | @Bean 55 | public UpdateGenreUseCase updateGenreUseCase() { 56 | return new DefaultUpdateGenreUseCase(categoryGateway, genreGateway); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /application/src/test/java/com/fullcycle/admin/catalogo/application/category/delete/DeleteCategoryUseCaseTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.delete; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCaseTest; 4 | import com.fullcycle.admin.catalogo.domain.category.Category; 5 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 6 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.Mockito; 12 | 13 | import java.util.List; 14 | 15 | import static org.mockito.ArgumentMatchers.eq; 16 | import static org.mockito.Mockito.*; 17 | 18 | public class DeleteCategoryUseCaseTest extends UseCaseTest { 19 | 20 | @InjectMocks 21 | private DefaultDeleteCategoryUseCase useCase; 22 | 23 | @Mock 24 | private CategoryGateway categoryGateway; 25 | 26 | @Override 27 | protected List getMocks() { 28 | return List.of(categoryGateway); 29 | } 30 | 31 | @Test 32 | public void givenAValidId_whenCallsDeleteCategory_shouldBeOK() { 33 | final var aCategory = Category.newCategory("Filmes", "A categoria mais assistida", true); 34 | final var expectedId = aCategory.getId(); 35 | 36 | doNothing() 37 | .when(categoryGateway).deleteById(eq(expectedId)); 38 | 39 | Assertions.assertDoesNotThrow(() -> useCase.execute(expectedId.getValue())); 40 | 41 | Mockito.verify(categoryGateway, times(1)).deleteById(eq(expectedId)); 42 | } 43 | 44 | @Test 45 | public void givenAInvalidId_whenCallsDeleteCategory_shouldBeOK() { 46 | final var expectedId = CategoryID.from("123"); 47 | 48 | doNothing() 49 | .when(categoryGateway).deleteById(eq(expectedId)); 50 | 51 | Assertions.assertDoesNotThrow(() -> useCase.execute(expectedId.getValue())); 52 | 53 | Mockito.verify(categoryGateway, times(1)).deleteById(eq(expectedId)); 54 | } 55 | 56 | @Test 57 | public void givenAValidId_whenGatewayThrowsException_shouldReturnException() { 58 | final var aCategory = Category.newCategory("Filmes", "A categoria mais assistida", true); 59 | final var expectedId = aCategory.getId(); 60 | 61 | doThrow(new IllegalStateException("Gateway error")) 62 | .when(categoryGateway).deleteById(eq(expectedId)); 63 | 64 | Assertions.assertThrows(IllegalStateException.class, () -> useCase.execute(expectedId.getValue())); 65 | 66 | Mockito.verify(categoryGateway, times(1)).deleteById(eq(expectedId)); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /application/src/test/java/com/fullcycle/admin/catalogo/application/genre/delete/DeleteGenreUseCaseTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.delete; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCaseTest; 4 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 5 | import com.fullcycle.admin.catalogo.domain.genre.GenreGateway; 6 | import com.fullcycle.admin.catalogo.domain.genre.GenreID; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.Mockito; 12 | 13 | import java.util.List; 14 | 15 | import static org.mockito.ArgumentMatchers.any; 16 | import static org.mockito.Mockito.*; 17 | 18 | public class DeleteGenreUseCaseTest extends UseCaseTest { 19 | 20 | @InjectMocks 21 | private DefaultDeleteGenreUseCase useCase; 22 | 23 | @Mock 24 | private GenreGateway genreGateway; 25 | 26 | @Override 27 | protected List getMocks() { 28 | return List.of(genreGateway); 29 | } 30 | 31 | @Test 32 | public void givenAValidGenreId_whenCallsDeleteGenre_shouldDeleteGenre() { 33 | // given 34 | final var aGenre = Genre.newGenre("Ação", true); 35 | 36 | final var expectedId = aGenre.getId(); 37 | 38 | doNothing() 39 | .when(genreGateway).deleteById(any()); 40 | 41 | // when 42 | Assertions.assertDoesNotThrow(() -> useCase.execute(expectedId.getValue())); 43 | 44 | // when 45 | Mockito.verify(genreGateway, times(1)).deleteById(expectedId); 46 | } 47 | 48 | @Test 49 | public void givenAnInvalidGenreId_whenCallsDeleteGenre_shouldBeOk() { 50 | // given 51 | final var expectedId = GenreID.from("123"); 52 | 53 | doNothing() 54 | .when(genreGateway).deleteById(any()); 55 | 56 | // when 57 | Assertions.assertDoesNotThrow(() -> useCase.execute(expectedId.getValue())); 58 | 59 | // when 60 | Mockito.verify(genreGateway, times(1)).deleteById(expectedId); 61 | } 62 | 63 | @Test 64 | public void givenAValidGenreId_whenCallsDeleteGenreAndGatewayThrowsUnexpectedError_shouldReceiveException() { 65 | // given 66 | final var aGenre = Genre.newGenre("Ação", true); 67 | final var expectedId = aGenre.getId(); 68 | 69 | doThrow(new IllegalStateException("Gateway error")) 70 | .when(genreGateway).deleteById(any()); 71 | 72 | // when 73 | Assertions.assertThrows(IllegalStateException.class, () -> { 74 | useCase.execute(expectedId.getValue()); 75 | }); 76 | 77 | // when 78 | Mockito.verify(genreGateway, times(1)).deleteById(expectedId); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/application/category/delete/DeleteCategoryUseCaseIT.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.delete; 2 | 3 | import com.fullcycle.admin.catalogo.IntegrationTest; 4 | import com.fullcycle.admin.catalogo.domain.category.Category; 5 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 6 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 7 | import com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryJpaEntity; 8 | import com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryRepository; 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | import org.mockito.Mockito; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.mock.mockito.SpyBean; 14 | 15 | import java.util.Arrays; 16 | 17 | import static org.mockito.ArgumentMatchers.eq; 18 | import static org.mockito.Mockito.doThrow; 19 | import static org.mockito.Mockito.times; 20 | 21 | @IntegrationTest 22 | public class DeleteCategoryUseCaseIT { 23 | 24 | @Autowired 25 | private DeleteCategoryUseCase useCase; 26 | 27 | @Autowired 28 | private CategoryRepository categoryRepository; 29 | 30 | @SpyBean 31 | private CategoryGateway categoryGateway; 32 | 33 | @Test 34 | public void givenAValidId_whenCallsDeleteCategory_shouldBeOK() { 35 | final var aCategory = Category.newCategory("Filmes", "A categoria mais assistida", true); 36 | final var expectedId = aCategory.getId(); 37 | 38 | save(aCategory); 39 | 40 | Assertions.assertEquals(1, categoryRepository.count()); 41 | 42 | Assertions.assertDoesNotThrow(() -> useCase.execute(expectedId.getValue())); 43 | 44 | Assertions.assertEquals(0, categoryRepository.count()); 45 | } 46 | 47 | @Test 48 | public void givenAInvalidId_whenCallsDeleteCategory_shouldBeOK() { 49 | final var expectedId = CategoryID.from("123"); 50 | 51 | Assertions.assertEquals(0, categoryRepository.count()); 52 | 53 | Assertions.assertDoesNotThrow(() -> useCase.execute(expectedId.getValue())); 54 | 55 | Assertions.assertEquals(0, categoryRepository.count()); 56 | } 57 | 58 | @Test 59 | public void givenAValidId_whenGatewayThrowsException_shouldReturnException() { 60 | final var aCategory = Category.newCategory("Filmes", "A categoria mais assistida", true); 61 | final var expectedId = aCategory.getId(); 62 | 63 | doThrow(new IllegalStateException("Gateway error")) 64 | .when(categoryGateway).deleteById(eq(expectedId)); 65 | 66 | Assertions.assertThrows(IllegalStateException.class, () -> useCase.execute(expectedId.getValue())); 67 | 68 | Mockito.verify(categoryGateway, times(1)).deleteById(eq(expectedId)); 69 | } 70 | 71 | private void save(final Category... aCategory) { 72 | categoryRepository.saveAllAndFlush( 73 | Arrays.stream(aCategory) 74 | .map(CategoryJpaEntity::from) 75 | .toList() 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/create/DefaultCreateGenreUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.create; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 4 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 5 | import com.fullcycle.admin.catalogo.domain.exceptions.NotificationException; 6 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 7 | import com.fullcycle.admin.catalogo.domain.genre.GenreGateway; 8 | import com.fullcycle.admin.catalogo.domain.validation.Error; 9 | import com.fullcycle.admin.catalogo.domain.validation.ValidationHandler; 10 | import com.fullcycle.admin.catalogo.domain.validation.handler.Notification; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.Objects; 15 | import java.util.stream.Collectors; 16 | 17 | public class DefaultCreateGenreUseCase extends CreateGenreUseCase { 18 | 19 | private final CategoryGateway categoryGateway; 20 | private final GenreGateway genreGateway; 21 | 22 | public DefaultCreateGenreUseCase( 23 | final CategoryGateway categoryGateway, 24 | final GenreGateway genreGateway 25 | ) { 26 | this.categoryGateway = Objects.requireNonNull(categoryGateway); 27 | this.genreGateway = Objects.requireNonNull(genreGateway); 28 | } 29 | 30 | @Override 31 | public CreateGenreOutput execute(final CreateGenreCommand aCommand) { 32 | final var aName = aCommand.name(); 33 | final var isActive = aCommand.isActive(); 34 | final var categories = toCategoryID(aCommand.categories()); 35 | 36 | final var notification = Notification.create(); 37 | notification.append(validateCategories(categories)); 38 | 39 | final var aGenre = notification.validate(() -> Genre.newGenre(aName, isActive)); 40 | 41 | if (notification.hasError()) { 42 | throw new NotificationException("Could not create Aggregate Genre", notification); 43 | } 44 | 45 | aGenre.addCategories(categories); 46 | 47 | return CreateGenreOutput.from(this.genreGateway.create(aGenre)); 48 | } 49 | 50 | private ValidationHandler validateCategories(final List ids) { 51 | final var notification = Notification.create(); 52 | if (ids == null || ids.isEmpty()) { 53 | return notification; 54 | } 55 | 56 | final var retrievedIds = categoryGateway.existsByIds(ids); 57 | 58 | if (ids.size() != retrievedIds.size()) { 59 | final var missingIds = new ArrayList<>(ids); 60 | missingIds.removeAll(retrievedIds); 61 | 62 | final var missingIdsMessage = missingIds.stream() 63 | .map(CategoryID::getValue) 64 | .collect(Collectors.joining(", ")); 65 | 66 | notification.append(new Error("Some categories could not be found: %s".formatted(missingIdsMessage))); 67 | } 68 | 69 | return notification; 70 | } 71 | 72 | private List toCategoryID(final List categories) { 73 | return categories.stream() 74 | .map(CategoryID::from) 75 | .toList(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/genre/GenreMySQLGateway.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.genre; 2 | 3 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 4 | import com.fullcycle.admin.catalogo.domain.genre.GenreGateway; 5 | import com.fullcycle.admin.catalogo.domain.genre.GenreID; 6 | import com.fullcycle.admin.catalogo.domain.pagination.Pagination; 7 | import com.fullcycle.admin.catalogo.domain.pagination.SearchQuery; 8 | import com.fullcycle.admin.catalogo.infrastructure.genre.persistence.GenreJpaEntity; 9 | import com.fullcycle.admin.catalogo.infrastructure.genre.persistence.GenreRepository; 10 | import com.fullcycle.admin.catalogo.infrastructure.utils.SpecificationUtils; 11 | import org.springframework.data.domain.PageRequest; 12 | import org.springframework.data.domain.Sort; 13 | import org.springframework.data.jpa.domain.Specification; 14 | import org.springframework.stereotype.Component; 15 | 16 | import java.util.Objects; 17 | import java.util.Optional; 18 | 19 | import static org.springframework.data.jpa.domain.Specification.where; 20 | 21 | @Component 22 | public class GenreMySQLGateway implements GenreGateway { 23 | 24 | private final GenreRepository genreRepository; 25 | 26 | public GenreMySQLGateway(final GenreRepository genreRepository) { 27 | this.genreRepository = Objects.requireNonNull(genreRepository); 28 | } 29 | 30 | @Override 31 | public Genre create(final Genre aGenre) { 32 | return save(aGenre); 33 | } 34 | 35 | @Override 36 | public void deleteById(final GenreID anId) { 37 | final var aGenreId = anId.getValue(); 38 | if (this.genreRepository.existsById(aGenreId)) { 39 | this.genreRepository.deleteById(aGenreId); 40 | } 41 | } 42 | 43 | @Override 44 | public Optional findById(final GenreID anId) { 45 | return this.genreRepository.findById(anId.getValue()) 46 | .map(GenreJpaEntity::toAggregate); 47 | } 48 | 49 | @Override 50 | public Genre update(final Genre aGenre) { 51 | return save(aGenre); 52 | } 53 | 54 | @Override 55 | public Pagination findAll(final SearchQuery aQuery) { 56 | final var page = PageRequest.of( 57 | aQuery.page(), 58 | aQuery.perPage(), 59 | Sort.by(Sort.Direction.fromString(aQuery.direction()), aQuery.sort()) 60 | ); 61 | 62 | final var where = Optional.ofNullable(aQuery.terms()) 63 | .filter(str -> !str.isBlank()) 64 | .map(this::assembleSpecification) 65 | .orElse(null); 66 | 67 | final var pageResult = 68 | this.genreRepository.findAll(where(where), page); 69 | 70 | return new Pagination<>( 71 | pageResult.getNumber(), 72 | pageResult.getSize(), 73 | pageResult.getTotalElements(), 74 | pageResult.map(GenreJpaEntity::toAggregate).toList() 75 | ); 76 | } 77 | 78 | private Genre save(final Genre aGenre) { 79 | return this.genreRepository.save(GenreJpaEntity.from(aGenre)) 80 | .toAggregate(); 81 | } 82 | 83 | private Specification assembleSpecification(final String terms) { 84 | return SpecificationUtils.like("name", terms); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/application/genre/retrieve/get/GetGenreByIdUseCaseIT.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.retrieve.get; 2 | 3 | import com.fullcycle.admin.catalogo.IntegrationTest; 4 | import com.fullcycle.admin.catalogo.domain.category.Category; 5 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 6 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 7 | import com.fullcycle.admin.catalogo.domain.exceptions.NotFoundException; 8 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 9 | import com.fullcycle.admin.catalogo.domain.genre.GenreGateway; 10 | import com.fullcycle.admin.catalogo.domain.genre.GenreID; 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.Test; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | 15 | import java.util.List; 16 | 17 | @IntegrationTest 18 | public class GetGenreByIdUseCaseIT { 19 | 20 | @Autowired 21 | private GetGenreByIdUseCase useCase; 22 | 23 | @Autowired 24 | private CategoryGateway categoryGateway; 25 | 26 | @Autowired 27 | private GenreGateway genreGateway; 28 | 29 | @Test 30 | public void givenAValidId_whenCallsGetGenre_shouldReturnGenre() { 31 | // given 32 | final var series = 33 | categoryGateway.create(Category.newCategory("Séries", null, true)); 34 | 35 | final var filmes = 36 | categoryGateway.create(Category.newCategory("Filmes", null, true)); 37 | 38 | final var expectedName = "Ação"; 39 | final var expectedIsActive = true; 40 | final var expectedCategories = List.of(series.getId(), filmes.getId()); 41 | 42 | final var aGenre = genreGateway.create( 43 | Genre.newGenre(expectedName, expectedIsActive) 44 | .addCategories(expectedCategories) 45 | ); 46 | 47 | final var expectedId = aGenre.getId(); 48 | 49 | // when 50 | final var actualGenre = useCase.execute(expectedId.getValue()); 51 | 52 | // then 53 | Assertions.assertEquals(expectedId.getValue(), actualGenre.id()); 54 | Assertions.assertEquals(expectedName, actualGenre.name()); 55 | Assertions.assertEquals(expectedIsActive, actualGenre.isActive()); 56 | Assertions.assertTrue( 57 | expectedCategories.size() == actualGenre.categories().size() 58 | && asString(expectedCategories).containsAll(actualGenre.categories()) 59 | ); 60 | Assertions.assertEquals(aGenre.getCreatedAt(), actualGenre.createdAt()); 61 | Assertions.assertEquals(aGenre.getUpdatedAt(), actualGenre.updatedAt()); 62 | Assertions.assertEquals(aGenre.getDeletedAt(), actualGenre.deletedAt()); 63 | } 64 | 65 | @Test 66 | public void givenAValidId_whenCallsGetGenreAndDoesNotExists_shouldReturnNotFound() { 67 | // given 68 | final var expectedErrorMessage = "Genre with ID 123 was not found"; 69 | 70 | final var expectedId = GenreID.from("123"); 71 | 72 | // when 73 | final var actualException = Assertions.assertThrows(NotFoundException.class, () -> { 74 | useCase.execute(expectedId.getValue()); 75 | }); 76 | 77 | // then 78 | Assertions.assertEquals(expectedErrorMessage, actualException.getMessage()); 79 | } 80 | 81 | private List asString(final List ids) { 82 | return ids.stream() 83 | .map(CategoryID::getValue) 84 | .toList(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /application/src/test/java/com/fullcycle/admin/catalogo/application/genre/retrieve/get/GetGenreByIdUseCaseTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.retrieve.get; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCaseTest; 4 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 5 | import com.fullcycle.admin.catalogo.domain.exceptions.NotFoundException; 6 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 7 | import com.fullcycle.admin.catalogo.domain.genre.GenreGateway; 8 | import com.fullcycle.admin.catalogo.domain.genre.GenreID; 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.mockito.Mockito; 14 | 15 | import java.util.List; 16 | import java.util.Optional; 17 | 18 | import static org.mockito.ArgumentMatchers.any; 19 | import static org.mockito.ArgumentMatchers.eq; 20 | import static org.mockito.Mockito.times; 21 | import static org.mockito.Mockito.when; 22 | 23 | public class GetGenreByIdUseCaseTest extends UseCaseTest { 24 | 25 | @InjectMocks 26 | private DefaultGetGenreByIdUseCase useCase; 27 | 28 | @Mock 29 | private GenreGateway genreGateway; 30 | 31 | @Override 32 | protected List getMocks() { 33 | return List.of(genreGateway); 34 | } 35 | 36 | @Test 37 | public void givenAValidId_whenCallsGetGenre_shouldReturnGenre() { 38 | // given 39 | final var expectedName = "Ação"; 40 | final var expectedIsActive = true; 41 | final var expectedCategories = List.of( 42 | CategoryID.from("123"), 43 | CategoryID.from("456") 44 | ); 45 | 46 | final var aGenre = Genre.newGenre(expectedName, expectedIsActive) 47 | .addCategories(expectedCategories); 48 | 49 | final var expectedId = aGenre.getId(); 50 | 51 | when(genreGateway.findById(any())) 52 | .thenReturn(Optional.of(aGenre)); 53 | // when 54 | final var actualGenre = useCase.execute(expectedId.getValue()); 55 | 56 | // then 57 | Assertions.assertEquals(expectedId.getValue(), actualGenre.id()); 58 | Assertions.assertEquals(expectedName, actualGenre.name()); 59 | Assertions.assertEquals(expectedIsActive, actualGenre.isActive()); 60 | Assertions.assertEquals(asString(expectedCategories), actualGenre.categories()); 61 | Assertions.assertEquals(aGenre.getCreatedAt(), actualGenre.createdAt()); 62 | Assertions.assertEquals(aGenre.getUpdatedAt(), actualGenre.updatedAt()); 63 | Assertions.assertEquals(aGenre.getDeletedAt(), actualGenre.deletedAt()); 64 | 65 | Mockito.verify(genreGateway, times(1)).findById(eq(expectedId)); 66 | } 67 | 68 | @Test 69 | public void givenAValidId_whenCallsGetGenreAndDoesNotExists_shouldReturnNotFound() { 70 | // given 71 | final var expectedErrorMessage = "Genre with ID 123 was not found"; 72 | 73 | final var expectedId = GenreID.from("123"); 74 | 75 | when(genreGateway.findById(eq(expectedId))) 76 | .thenReturn(Optional.empty()); 77 | 78 | // when 79 | final var actualException = Assertions.assertThrows(NotFoundException.class, () -> { 80 | useCase.execute(expectedId.getValue()); 81 | }); 82 | 83 | // then 84 | Assertions.assertEquals(expectedErrorMessage, actualException.getMessage()); 85 | } 86 | 87 | private List asString(final List ids) { 88 | return ids.stream() 89 | .map(CategoryID::getValue) 90 | .toList(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/application/genre/retrieve/list/ListGenreUseCaseIT.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.retrieve.list; 2 | 3 | import com.fullcycle.admin.catalogo.IntegrationTest; 4 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 5 | import com.fullcycle.admin.catalogo.domain.genre.GenreGateway; 6 | import com.fullcycle.admin.catalogo.domain.pagination.SearchQuery; 7 | import com.fullcycle.admin.catalogo.infrastructure.genre.persistence.GenreJpaEntity; 8 | import com.fullcycle.admin.catalogo.infrastructure.genre.persistence.GenreRepository; 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | 13 | import java.util.List; 14 | 15 | @IntegrationTest 16 | public class ListGenreUseCaseIT { 17 | 18 | @Autowired 19 | private ListGenreUseCase useCase; 20 | 21 | @Autowired 22 | private GenreGateway genreGateway; 23 | 24 | @Autowired 25 | private GenreRepository genreRepository; 26 | 27 | @Test 28 | public void givenAValidQuery_whenCallsListGenre_shouldReturnGenres() { 29 | // given 30 | final var genres = List.of( 31 | Genre.newGenre("Ação", true), 32 | Genre.newGenre("Aventura", true) 33 | ); 34 | 35 | genreRepository.saveAllAndFlush( 36 | genres.stream() 37 | .map(GenreJpaEntity::from) 38 | .toList() 39 | ); 40 | 41 | final var expectedPage = 0; 42 | final var expectedPerPage = 10; 43 | final var expectedTerms = "A"; 44 | final var expectedSort = "createdAt"; 45 | final var expectedDirection = "asc"; 46 | final var expectedTotal = 2; 47 | 48 | final var expectedItems = genres.stream() 49 | .map(GenreListOutput::from) 50 | .toList(); 51 | 52 | final var aQuery = 53 | new SearchQuery(expectedPage, expectedPerPage, expectedTerms, expectedSort, expectedDirection); 54 | 55 | // when 56 | final var actualOutput = useCase.execute(aQuery); 57 | 58 | // then 59 | Assertions.assertEquals(expectedPage, actualOutput.currentPage()); 60 | Assertions.assertEquals(expectedPerPage, actualOutput.perPage()); 61 | Assertions.assertEquals(expectedTotal, actualOutput.total()); 62 | Assertions.assertTrue( 63 | expectedItems.size() == actualOutput.items().size() 64 | && expectedItems.containsAll(actualOutput.items()) 65 | ); 66 | } 67 | 68 | @Test 69 | public void givenAValidQuery_whenCallsListGenreAndResultIsEmpty_shouldReturnGenres() { 70 | // given 71 | final var genres = List.of(); 72 | 73 | final var expectedPage = 0; 74 | final var expectedPerPage = 10; 75 | final var expectedTerms = "A"; 76 | final var expectedSort = "createdAt"; 77 | final var expectedDirection = "asc"; 78 | final var expectedTotal = 0; 79 | 80 | final var expectedItems = List.of(); 81 | 82 | final var aQuery = 83 | new SearchQuery(expectedPage, expectedPerPage, expectedTerms, expectedSort, expectedDirection); 84 | 85 | // when 86 | final var actualOutput = useCase.execute(aQuery); 87 | 88 | // then 89 | Assertions.assertEquals(expectedPage, actualOutput.currentPage()); 90 | Assertions.assertEquals(expectedPerPage, actualOutput.perPage()); 91 | Assertions.assertEquals(expectedTotal, actualOutput.total()); 92 | Assertions.assertEquals(expectedItems, actualOutput.items()); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /application/src/test/java/com/fullcycle/admin/catalogo/application/category/retrieve/get/GetCategoryByIdUseCaseTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.retrieve.get; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCaseTest; 4 | import com.fullcycle.admin.catalogo.domain.category.Category; 5 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 6 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 7 | import com.fullcycle.admin.catalogo.domain.exceptions.NotFoundException; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.Test; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | 13 | import java.util.List; 14 | import java.util.Optional; 15 | 16 | import static org.mockito.ArgumentMatchers.eq; 17 | import static org.mockito.Mockito.when; 18 | 19 | public class GetCategoryByIdUseCaseTest extends UseCaseTest { 20 | 21 | @InjectMocks 22 | private DefaultGetCategoryByIdUseCase useCase; 23 | 24 | @Mock 25 | private CategoryGateway categoryGateway; 26 | 27 | @Override 28 | protected List getMocks() { 29 | return List.of(categoryGateway); 30 | } 31 | 32 | @Test 33 | public void givenAValidId_whenCallsGetCategory_shouldReturnCategory() { 34 | final var expectedName = "Filmes"; 35 | final var expectedDescription = "A categoria mais assistida"; 36 | final var expectedIsActive = true; 37 | 38 | final var aCategory = 39 | Category.newCategory(expectedName, expectedDescription, expectedIsActive); 40 | 41 | final var expectedId = aCategory.getId(); 42 | 43 | when(categoryGateway.findById(eq(expectedId))) 44 | .thenReturn(Optional.of(aCategory.clone())); 45 | 46 | final var actualCategory = useCase.execute(expectedId.getValue()); 47 | 48 | Assertions.assertEquals(expectedId, actualCategory.id()); 49 | Assertions.assertEquals(expectedName, actualCategory.name()); 50 | Assertions.assertEquals(expectedDescription, actualCategory.description()); 51 | Assertions.assertEquals(expectedIsActive, actualCategory.isActive()); 52 | Assertions.assertEquals(aCategory.getCreatedAt(), actualCategory.createdAt()); 53 | Assertions.assertEquals(aCategory.getUpdatedAt(), actualCategory.updatedAt()); 54 | Assertions.assertEquals(aCategory.getDeletedAt(), actualCategory.deletedAt()); 55 | } 56 | 57 | @Test 58 | public void givenAInvalidId_whenCallsGetCategory_shouldReturnNotFound() { 59 | final var expectedErrorMessage = "Category with ID 123 was not found"; 60 | final var expectedId = CategoryID.from("123"); 61 | 62 | when(categoryGateway.findById(eq(expectedId))) 63 | .thenReturn(Optional.empty()); 64 | 65 | final var actualException = Assertions.assertThrows( 66 | NotFoundException.class, 67 | () -> useCase.execute(expectedId.getValue()) 68 | ); 69 | 70 | Assertions.assertEquals(expectedErrorMessage, actualException.getMessage()); 71 | } 72 | 73 | @Test 74 | public void givenAValidId_whenGatewayThrowsException_shouldReturnException() { 75 | final var expectedErrorMessage = "Gateway error"; 76 | final var expectedId = CategoryID.from("123"); 77 | 78 | when(categoryGateway.findById(eq(expectedId))) 79 | .thenThrow(new IllegalStateException(expectedErrorMessage)); 80 | 81 | final var actualException = Assertions.assertThrows( 82 | IllegalStateException.class, 83 | () -> useCase.execute(expectedId.getValue()) 84 | ); 85 | 86 | Assertions.assertEquals(expectedErrorMessage, actualException.getMessage()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/infrastructure/category/models/CategoryResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.category.models; 2 | 3 | import com.fullcycle.admin.catalogo.JacksonTest; 4 | import org.assertj.core.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.json.JacksonTester; 8 | 9 | import java.time.Instant; 10 | 11 | @JacksonTest 12 | public class CategoryResponseTest { 13 | 14 | @Autowired 15 | private JacksonTester json; 16 | 17 | @Test 18 | public void testMarshall() throws Exception { 19 | final var expectedId = "123"; 20 | final var expectedName = "Filmes"; 21 | final var expectedDescription = "A categoria mais assistida"; 22 | final var expectedIsActive = false; 23 | final var expectedCreatedAt = Instant.now(); 24 | final var expectedUpdatedAt = Instant.now(); 25 | final var expectedDeletedAt = Instant.now(); 26 | 27 | final var response = new CategoryResponse( 28 | expectedId, 29 | expectedName, 30 | expectedDescription, 31 | expectedIsActive, 32 | expectedCreatedAt, 33 | expectedUpdatedAt, 34 | expectedDeletedAt 35 | ); 36 | 37 | final var actualJson = this.json.write(response); 38 | 39 | Assertions.assertThat(actualJson) 40 | .hasJsonPathValue("$.id", expectedId) 41 | .hasJsonPathValue("$.name", expectedName) 42 | .hasJsonPathValue("$.description", expectedDescription) 43 | .hasJsonPathValue("$.is_active", expectedIsActive) 44 | .hasJsonPathValue("$.created_at", expectedCreatedAt.toString()) 45 | .hasJsonPathValue("$.deleted_at", expectedDeletedAt.toString()) 46 | .hasJsonPathValue("$.updated_at", expectedUpdatedAt.toString()); 47 | } 48 | 49 | @Test 50 | public void testUnmarshall() throws Exception { 51 | final var expectedId = "123"; 52 | final var expectedName = "Filmes"; 53 | final var expectedDescription = "A categoria mais assistida"; 54 | final var expectedIsActive = false; 55 | final var expectedCreatedAt = Instant.now(); 56 | final var expectedUpdatedAt = Instant.now(); 57 | final var expectedDeletedAt = Instant.now(); 58 | 59 | final var json = """ 60 | { 61 | "id": "%s", 62 | "name": "%s", 63 | "description": "%s", 64 | "is_active": %s, 65 | "created_at": "%s", 66 | "deleted_at": "%s", 67 | "updated_at": "%s" 68 | } 69 | """.formatted( 70 | expectedId, 71 | expectedName, 72 | expectedDescription, 73 | expectedIsActive, 74 | expectedCreatedAt.toString(), 75 | expectedDeletedAt.toString(), 76 | expectedUpdatedAt.toString() 77 | ); 78 | 79 | final var actualJson = this.json.parse(json); 80 | 81 | Assertions.assertThat(actualJson) 82 | .hasFieldOrPropertyWithValue("id", expectedId) 83 | .hasFieldOrPropertyWithValue("name", expectedName) 84 | .hasFieldOrPropertyWithValue("description", expectedDescription) 85 | .hasFieldOrPropertyWithValue("active", expectedIsActive) 86 | .hasFieldOrPropertyWithValue("createdAt", expectedCreatedAt) 87 | .hasFieldOrPropertyWithValue("deletedAt", expectedDeletedAt) 88 | .hasFieldOrPropertyWithValue("updatedAt", expectedUpdatedAt); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /application/src/main/java/com/fullcycle/admin/catalogo/application/genre/update/DefaultUpdateGenreUseCase.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.update; 2 | 3 | import com.fullcycle.admin.catalogo.domain.Identifier; 4 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 5 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 6 | import com.fullcycle.admin.catalogo.domain.exceptions.DomainException; 7 | import com.fullcycle.admin.catalogo.domain.exceptions.NotFoundException; 8 | import com.fullcycle.admin.catalogo.domain.exceptions.NotificationException; 9 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 10 | import com.fullcycle.admin.catalogo.domain.genre.GenreGateway; 11 | import com.fullcycle.admin.catalogo.domain.genre.GenreID; 12 | import com.fullcycle.admin.catalogo.domain.validation.Error; 13 | import com.fullcycle.admin.catalogo.domain.validation.ValidationHandler; 14 | import com.fullcycle.admin.catalogo.domain.validation.handler.Notification; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.Objects; 19 | import java.util.function.Supplier; 20 | import java.util.stream.Collectors; 21 | 22 | public class DefaultUpdateGenreUseCase extends UpdateGenreUseCase { 23 | 24 | private final CategoryGateway categoryGateway; 25 | private final GenreGateway genreGateway; 26 | 27 | public DefaultUpdateGenreUseCase( 28 | final CategoryGateway categoryGateway, 29 | final GenreGateway genreGateway 30 | ) { 31 | this.categoryGateway = Objects.requireNonNull(categoryGateway); 32 | this.genreGateway = Objects.requireNonNull(genreGateway); 33 | } 34 | 35 | @Override 36 | public UpdateGenreOutput execute(final UpdateGenreCommand aCommand) { 37 | final var anId = GenreID.from(aCommand.id()); 38 | final var aName = aCommand.name(); 39 | final var isActive = aCommand.isActive(); 40 | final var categories = toCategoryId(aCommand.categories()); 41 | 42 | final var aGenre = this.genreGateway.findById(anId) 43 | .orElseThrow(notFound(anId)); 44 | 45 | final var notification = Notification.create(); 46 | notification.append(validateCategories(categories)); 47 | notification.validate(() -> aGenre.update(aName, isActive, categories)); 48 | 49 | if (notification.hasError()) { 50 | throw new NotificationException( 51 | "Could not update Aggregate Genre %s".formatted(aCommand.id()), notification 52 | ); 53 | } 54 | 55 | return UpdateGenreOutput.from(this.genreGateway.update(aGenre)); 56 | } 57 | 58 | private ValidationHandler validateCategories(List ids) { 59 | final var notification = Notification.create(); 60 | if (ids == null || ids.isEmpty()) { 61 | return notification; 62 | } 63 | 64 | final var retrievedIds = categoryGateway.existsByIds(ids); 65 | 66 | if (ids.size() != retrievedIds.size()) { 67 | final var missingIds = new ArrayList<>(ids); 68 | missingIds.removeAll(retrievedIds); 69 | 70 | final var missingIdsMessage = missingIds.stream() 71 | .map(CategoryID::getValue) 72 | .collect(Collectors.joining(", ")); 73 | 74 | notification.append(new Error("Some categories could not be found: %s".formatted(missingIdsMessage))); 75 | } 76 | 77 | return notification; 78 | } 79 | 80 | private Supplier notFound(final Identifier anId) { 81 | return () -> NotFoundException.with(Genre.class, anId); 82 | } 83 | 84 | private List toCategoryId(final List categories) { 85 | return categories.stream() 86 | .map(CategoryID::from) 87 | .toList(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/infrastructure/category/persistence/CategoryRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.category.persistence; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.Category; 4 | import com.fullcycle.admin.catalogo.MySQLGatewayTest; 5 | import org.hibernate.PropertyValueException; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.dao.DataIntegrityViolationException; 10 | 11 | @MySQLGatewayTest 12 | public class CategoryRepositoryTest { 13 | 14 | @Autowired 15 | private CategoryRepository categoryRepository; 16 | 17 | @Test 18 | public void givenAnInvalidNullName_whenCallsSave_shouldReturnError() { 19 | final var expectedPropertyName = "name"; 20 | final var expectedMessage = "not-null property references a null or transient value : com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryJpaEntity.name"; 21 | 22 | final var aCategory = Category.newCategory("Filmes", "A categoria mais assistida", true); 23 | 24 | final var anEntity = CategoryJpaEntity.from(aCategory); 25 | anEntity.setName(null); 26 | 27 | final var actualException = 28 | Assertions.assertThrows(DataIntegrityViolationException.class, () -> categoryRepository.save(anEntity)); 29 | 30 | final var actualCause = 31 | Assertions.assertInstanceOf(PropertyValueException.class, actualException.getCause()); 32 | 33 | Assertions.assertEquals(expectedPropertyName, actualCause.getPropertyName()); 34 | Assertions.assertEquals(expectedMessage, actualCause.getMessage()); 35 | } 36 | 37 | @Test 38 | public void givenAnInvalidNullCreatedAt_whenCallsSave_shouldReturnError() { 39 | final var expectedPropertyName = "createdAt"; 40 | final var expectedMessage = "not-null property references a null or transient value : com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryJpaEntity.createdAt"; 41 | 42 | final var aCategory = Category.newCategory("Filmes", "A categoria mais assistida", true); 43 | 44 | final var anEntity = CategoryJpaEntity.from(aCategory); 45 | anEntity.setCreatedAt(null); 46 | 47 | final var actualException = 48 | Assertions.assertThrows(DataIntegrityViolationException.class, () -> categoryRepository.save(anEntity)); 49 | 50 | final var actualCause = 51 | Assertions.assertInstanceOf(PropertyValueException.class, actualException.getCause()); 52 | 53 | Assertions.assertEquals(expectedPropertyName, actualCause.getPropertyName()); 54 | Assertions.assertEquals(expectedMessage, actualCause.getMessage()); 55 | } 56 | 57 | @Test 58 | public void givenAnInvalidNullUpdatedAt_whenCallsSave_shouldReturnError() { 59 | final var expectedPropertyName = "updatedAt"; 60 | final var expectedMessage = "not-null property references a null or transient value : com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryJpaEntity.updatedAt"; 61 | 62 | final var aCategory = Category.newCategory("Filmes", "A categoria mais assistida", true); 63 | 64 | final var anEntity = CategoryJpaEntity.from(aCategory); 65 | anEntity.setUpdatedAt(null); 66 | 67 | final var actualException = 68 | Assertions.assertThrows(DataIntegrityViolationException.class, () -> categoryRepository.save(anEntity)); 69 | 70 | final var actualCause = 71 | Assertions.assertInstanceOf(PropertyValueException.class, actualException.getCause()); 72 | 73 | Assertions.assertEquals(expectedPropertyName, actualCause.getPropertyName()); 74 | Assertions.assertEquals(expectedMessage, actualCause.getMessage()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/application/category/retrieve/get/GetCategoryByIdUseCaseIT.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.retrieve.get; 2 | 3 | import com.fullcycle.admin.catalogo.IntegrationTest; 4 | import com.fullcycle.admin.catalogo.domain.category.Category; 5 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 6 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 7 | import com.fullcycle.admin.catalogo.domain.exceptions.NotFoundException; 8 | import com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryJpaEntity; 9 | import com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryRepository; 10 | import org.junit.jupiter.api.Assertions; 11 | import org.junit.jupiter.api.Test; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.mock.mockito.SpyBean; 14 | 15 | import java.util.Arrays; 16 | 17 | import static org.mockito.ArgumentMatchers.eq; 18 | import static org.mockito.Mockito.doThrow; 19 | 20 | @IntegrationTest 21 | public class GetCategoryByIdUseCaseIT { 22 | 23 | @Autowired 24 | private GetCategoryByIdUseCase useCase; 25 | 26 | @Autowired 27 | private CategoryRepository categoryRepository; 28 | 29 | @SpyBean 30 | private CategoryGateway categoryGateway; 31 | 32 | @Test 33 | public void givenAValidId_whenCallsGetCategory_shouldReturnCategory() { 34 | final var expectedName = "Filmes"; 35 | final var expectedDescription = "A categoria mais assistida"; 36 | final var expectedIsActive = true; 37 | 38 | final var aCategory = 39 | Category.newCategory(expectedName, expectedDescription, expectedIsActive); 40 | 41 | final var expectedId = aCategory.getId(); 42 | 43 | save(aCategory); 44 | 45 | final var actualCategory = useCase.execute(expectedId.getValue()); 46 | 47 | Assertions.assertEquals(expectedId, actualCategory.id()); 48 | Assertions.assertEquals(expectedName, actualCategory.name()); 49 | Assertions.assertEquals(expectedDescription, actualCategory.description()); 50 | Assertions.assertEquals(expectedIsActive, actualCategory.isActive()); 51 | Assertions.assertEquals(aCategory.getCreatedAt(), actualCategory.createdAt()); 52 | Assertions.assertEquals(aCategory.getUpdatedAt(), actualCategory.updatedAt()); 53 | Assertions.assertEquals(aCategory.getDeletedAt(), actualCategory.deletedAt()); 54 | } 55 | 56 | @Test 57 | public void givenAInvalidId_whenCallsGetCategory_shouldReturnNotFound() { 58 | final var expectedErrorMessage = "Category with ID 123 was not found"; 59 | final var expectedId = CategoryID.from("123"); 60 | 61 | final var actualException = Assertions.assertThrows( 62 | NotFoundException.class, 63 | () -> useCase.execute(expectedId.getValue()) 64 | ); 65 | 66 | Assertions.assertEquals(expectedErrorMessage, actualException.getMessage()); 67 | } 68 | 69 | @Test 70 | public void givenAValidId_whenGatewayThrowsException_shouldReturnException() { 71 | final var expectedErrorMessage = "Gateway error"; 72 | final var expectedId = CategoryID.from("123"); 73 | 74 | doThrow(new IllegalStateException(expectedErrorMessage)) 75 | .when(categoryGateway).findById(eq(expectedId)); 76 | 77 | final var actualException = Assertions.assertThrows( 78 | IllegalStateException.class, 79 | () -> useCase.execute(expectedId.getValue()) 80 | ); 81 | 82 | Assertions.assertEquals(expectedErrorMessage, actualException.getMessage()); 83 | } 84 | 85 | private void save(final Category... aCategory) { 86 | categoryRepository.saveAllAndFlush( 87 | Arrays.stream(aCategory) 88 | .map(CategoryJpaEntity::from) 89 | .toList() 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 |   4 | 5 |

6 |

🚀 Microserviço: Admin do Catálogo de Vídeos com Java

7 |

8 | Microserviço referente ao backend da Administração do Catálogo de Vídeos
9 | Utilizando Clean Architecture, DDD, TDD e as boas práticas atuais de mercado 10 |

11 |
12 |
13 | 14 | ## Ferramentas necessárias 15 | 16 | - JDK 17 17 | - IDE de sua preferência 18 | - Docker 19 | 20 | ## Como executar? 21 | 22 | 1. Clonar o repositório: 23 | ```sh 24 | git clone https://github.com/codeedu/micro-admin-videos-java.git 25 | ``` 26 | 27 | 2. Subir o banco de dados MySQL com Docker: 28 | ```shell 29 | docker-compose up -d 30 | ``` 31 | 32 | 3. Executar as migrações do MySQL com o Flyway: 33 | ```shell 34 | ./gradlew flywayMigrate 35 | ``` 36 | 37 | 4. Executar a aplicação como SpringBoot app: 38 | ```shell 39 | ./gradlew bootRun 40 | ``` 41 | 42 | > Também é possível executar como uma aplicação Java através do 43 | > método main() na classe Main.java 44 | ## Banco de dados 45 | 46 | O banco de dados principal é um MySQL e para subir localmente vamos utilizar o 47 | Docker. Execute o comando a seguir para subir o MySQL: 48 | 49 | ```shell 50 | docker-compose up -d 51 | ``` 52 | 53 | Pronto! Aguarde que em instantes o MySQL irá estar pronto para ser consumido 54 | na porta 3306. 55 | 56 | ### Migrações do banco de dados com Flyway 57 | 58 | #### Executar as migrações 59 | 60 | Caso seja a primeira vez que esteja subindo o banco de dados, é necessário 61 | executar as migrações SQL com a ferramenta `flyway`. 62 | Execute o comando a seguir para executar as migrações: 63 | 64 | ```shell 65 | ./gradlew flywayMigrate 66 | ``` 67 | 68 | Pronto! Agora sim o banco de dados MySQL está pronto para ser utilizado. 69 | 70 |
71 | 72 | #### Limpar as migrações do banco 73 | 74 | É possível limpar (deletar todas as tabelas) seu banco de dados, basta 75 | executar o seguinte comando: 76 | 77 | ```shell 78 | ./gradlew flywayClean 79 | ``` 80 | 81 | MAS lembre-se: "Grandes poderes, vem grandes responsabilidades". 82 | 83 |
84 | 85 | #### Reparando as migrações do banco 86 | 87 | Existe duas maneiras de gerar uma inconsistência no Flyway deixando ele no estado de reparação: 88 | 89 | 1. Algum arquivo SQL de migração com erro; 90 | 2. Algum arquivo de migração já aplicado foi alterado (modificando o `checksum`). 91 | 92 | Quando isso acontecer o flyway ficará em um estado de reparação 93 | com um registro na tabela `flyway_schema_history` com erro (`sucesso = 0`). 94 | 95 | Para executar a reparação, corrija os arquivos e execute: 96 | ```shell 97 | ./gradlew flywayRepair 98 | ``` 99 | 100 | Com o comando acima o Flyway limpará os registros com erro da tabela `flyway_schema_history`, 101 | na sequência execute o comando FlywayMigrate para tentar migrar-los novamente. 102 | 103 |
104 | 105 | #### Outros comandos úteis do Flyway 106 | 107 | Além dos comandos já exibidos, temos alguns outros muito úteis como o info e o validate: 108 | 109 | ```shell 110 | ./gradlew flywayInfo 111 | ./gradlew flywayValidate 112 | ``` 113 | 114 | Para saber todos os comandos disponíveis: [Flyway Gradle Plugin](https://flywaydb.org/documentation/usage/gradle/info) 115 | 116 |
117 | 118 | #### Para executar os comandos em outro ambiente 119 | 120 | Lá no `build.gradle` configuramos o Flyway para lêr primeiro as variáveis de 121 | ambiente `FLYWAY_DB`, `FLYWAY_USER` e `FLYWAY_PASS` e depois usar um valor padrão 122 | caso não as encontre. Com isso, para apontar para outro ambiente basta sobrescrever 123 | essas variáveis na hora de executar os comandos, exemplo: 124 | 125 | ```shell 126 | FLYWAY_DB=jdbc:mysql://prod:3306/adm_videos FLYWAY_USER=root FLYWAY_PASS=123h1hu ./gradlew flywayValidate 127 | ``` -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/category/persistence/CategoryJpaEntity.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.category.persistence; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.Category; 4 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | import java.time.Instant; 11 | 12 | @Entity(name = "Category") 13 | @Table(name = "category") 14 | public class CategoryJpaEntity { 15 | 16 | @Id 17 | private String id; 18 | 19 | @Column(name = "name", nullable = false) 20 | private String name; 21 | 22 | @Column(name = "description", length = 4000) 23 | private String description; 24 | 25 | @Column(name = "active", nullable = false) 26 | private boolean active; 27 | 28 | @Column(name = "created_at", nullable = false, columnDefinition = "DATETIME(6)") 29 | private Instant createdAt; 30 | 31 | @Column(name = "updated_at", nullable = false, columnDefinition = "DATETIME(6)") 32 | private Instant updatedAt; 33 | 34 | @Column(name = "deleted_at", columnDefinition = "DATETIME(6)") 35 | private Instant deletedAt; 36 | 37 | public CategoryJpaEntity() { 38 | } 39 | 40 | private CategoryJpaEntity( 41 | final String id, 42 | final String name, 43 | final String description, 44 | final boolean active, 45 | final Instant createdAt, 46 | final Instant updatedAt, 47 | final Instant deletedAt 48 | ) { 49 | this.id = id; 50 | this.name = name; 51 | this.description = description; 52 | this.active = active; 53 | this.createdAt = createdAt; 54 | this.updatedAt = updatedAt; 55 | this.deletedAt = deletedAt; 56 | } 57 | 58 | public static CategoryJpaEntity from(final Category aCategory) { 59 | return new CategoryJpaEntity( 60 | aCategory.getId().getValue(), 61 | aCategory.getName(), 62 | aCategory.getDescription(), 63 | aCategory.isActive(), 64 | aCategory.getCreatedAt(), 65 | aCategory.getUpdatedAt(), 66 | aCategory.getDeletedAt() 67 | ); 68 | } 69 | 70 | public Category toAggregate() { 71 | return Category.with( 72 | CategoryID.from(getId()), 73 | getName(), 74 | getDescription(), 75 | isActive(), 76 | getCreatedAt(), 77 | getUpdatedAt(), 78 | getDeletedAt() 79 | ); 80 | } 81 | 82 | public String getId() { 83 | return id; 84 | } 85 | 86 | public void setId(String id) { 87 | this.id = id; 88 | } 89 | 90 | public String getName() { 91 | return name; 92 | } 93 | 94 | public void setName(String name) { 95 | this.name = name; 96 | } 97 | 98 | public String getDescription() { 99 | return description; 100 | } 101 | 102 | public void setDescription(String description) { 103 | this.description = description; 104 | } 105 | 106 | public boolean isActive() { 107 | return active; 108 | } 109 | 110 | public void setActive(boolean active) { 111 | this.active = active; 112 | } 113 | 114 | public Instant getCreatedAt() { 115 | return createdAt; 116 | } 117 | 118 | public void setCreatedAt(Instant createdAt) { 119 | this.createdAt = createdAt; 120 | } 121 | 122 | public Instant getUpdatedAt() { 123 | return updatedAt; 124 | } 125 | 126 | public void setUpdatedAt(Instant updatedAt) { 127 | this.updatedAt = updatedAt; 128 | } 129 | 130 | public Instant getDeletedAt() { 131 | return deletedAt; 132 | } 133 | 134 | public void setDeletedAt(Instant deletedAt) { 135 | this.deletedAt = deletedAt; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/category/CategoryMySQLGateway.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.category; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.Category; 4 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 5 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 6 | import com.fullcycle.admin.catalogo.domain.pagination.Pagination; 7 | import com.fullcycle.admin.catalogo.domain.pagination.SearchQuery; 8 | import com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryJpaEntity; 9 | import com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryRepository; 10 | import org.springframework.data.domain.PageRequest; 11 | import org.springframework.data.domain.Sort; 12 | import org.springframework.data.domain.Sort.Direction; 13 | import org.springframework.data.jpa.domain.Specification; 14 | import org.springframework.stereotype.Component; 15 | 16 | import java.util.List; 17 | import java.util.Optional; 18 | import java.util.stream.StreamSupport; 19 | 20 | import static com.fullcycle.admin.catalogo.infrastructure.utils.SpecificationUtils.like; 21 | 22 | @Component 23 | public class CategoryMySQLGateway implements CategoryGateway { 24 | 25 | private final CategoryRepository repository; 26 | 27 | public CategoryMySQLGateway(final CategoryRepository repository) { 28 | this.repository = repository; 29 | } 30 | 31 | @Override 32 | public Category create(final Category aCategory) { 33 | return save(aCategory); 34 | } 35 | 36 | @Override 37 | public void deleteById(final CategoryID anId) { 38 | final String anIdValue = anId.getValue(); 39 | if (this.repository.existsById(anIdValue)) { 40 | this.repository.deleteById(anIdValue); 41 | } 42 | } 43 | 44 | @Override 45 | public Optional findById(final CategoryID anId) { 46 | return this.repository.findById(anId.getValue()) 47 | .map(CategoryJpaEntity::toAggregate); 48 | } 49 | 50 | @Override 51 | public Category update(final Category aCategory) { 52 | return save(aCategory); 53 | } 54 | 55 | @Override 56 | public Pagination findAll(final SearchQuery aQuery) { 57 | // Paginação 58 | final var page = PageRequest.of( 59 | aQuery.page(), 60 | aQuery.perPage(), 61 | Sort.by(Direction.fromString(aQuery.direction()), aQuery.sort()) 62 | ); 63 | 64 | // Busca dinamica pelo criterio terms (name ou description) 65 | final var specifications = Optional.ofNullable(aQuery.terms()) 66 | .filter(str -> !str.isBlank()) 67 | .map(this::assembleSpecification) 68 | .orElse(null); 69 | 70 | final var pageResult = 71 | this.repository.findAll(Specification.where(specifications), page); 72 | 73 | return new Pagination<>( 74 | pageResult.getNumber(), 75 | pageResult.getSize(), 76 | pageResult.getTotalElements(), 77 | pageResult.map(CategoryJpaEntity::toAggregate).toList() 78 | ); 79 | } 80 | 81 | @Override 82 | public List existsByIds(final Iterable categoryIDs) { 83 | final var ids = StreamSupport.stream(categoryIDs.spliterator(), false) 84 | .map(CategoryID::getValue) 85 | .toList(); 86 | return this.repository.existsByIds(ids).stream() 87 | .map(CategoryID::from) 88 | .toList(); 89 | } 90 | 91 | private Category save(final Category aCategory) { 92 | return this.repository.save(CategoryJpaEntity.from(aCategory)).toAggregate(); 93 | } 94 | 95 | private Specification assembleSpecification(final String str) { 96 | final Specification nameLike = like("name", str); 97 | final Specification descriptionLike = like("description", str); 98 | return nameLike.or(descriptionLike); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/api/CategoryAPI.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.api; 2 | 3 | import com.fullcycle.admin.catalogo.domain.pagination.Pagination; 4 | import com.fullcycle.admin.catalogo.infrastructure.category.models.CategoryListResponse; 5 | import com.fullcycle.admin.catalogo.infrastructure.category.models.CategoryResponse; 6 | import com.fullcycle.admin.catalogo.infrastructure.category.models.CreateCategoryRequest; 7 | import com.fullcycle.admin.catalogo.infrastructure.category.models.UpdateCategoryRequest; 8 | import io.swagger.v3.oas.annotations.Operation; 9 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 10 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 11 | import io.swagger.v3.oas.annotations.tags.Tag; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.MediaType; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | @RequestMapping(value = "categories") 18 | @Tag(name = "Categories") 19 | public interface CategoryAPI { 20 | 21 | @PostMapping( 22 | consumes = MediaType.APPLICATION_JSON_VALUE, 23 | produces = MediaType.APPLICATION_JSON_VALUE 24 | ) 25 | @Operation(summary = "Create a new category") 26 | @ApiResponses(value = { 27 | @ApiResponse(responseCode = "201", description = "Created successfully"), 28 | @ApiResponse(responseCode = "422", description = "A validation error was thrown"), 29 | @ApiResponse(responseCode = "500", description = "An internal server error was thrown"), 30 | }) 31 | ResponseEntity createCategory(@RequestBody CreateCategoryRequest input); 32 | 33 | @GetMapping 34 | @Operation(summary = "List all categories paginated") 35 | @ApiResponses(value = { 36 | @ApiResponse(responseCode = "200", description = "Listed successfully"), 37 | @ApiResponse(responseCode = "422", description = "A invalid parameter was received"), 38 | @ApiResponse(responseCode = "500", description = "An internal server error was thrown"), 39 | }) 40 | Pagination listCategories( 41 | @RequestParam(name = "search", required = false, defaultValue = "") final String search, 42 | @RequestParam(name = "page", required = false, defaultValue = "0") final int page, 43 | @RequestParam(name = "perPage", required = false, defaultValue = "10") final int perPage, 44 | @RequestParam(name = "sort", required = false, defaultValue = "name") final String sort, 45 | @RequestParam(name = "dir", required = false, defaultValue = "asc") final String direction 46 | ); 47 | 48 | @GetMapping( 49 | value = "{id}", 50 | produces = MediaType.APPLICATION_JSON_VALUE 51 | ) 52 | @Operation(summary = "Get a category by it's identifier") 53 | @ApiResponses(value = { 54 | @ApiResponse(responseCode = "200", description = "Category retrieved successfully"), 55 | @ApiResponse(responseCode = "404", description = "Category was not found"), 56 | @ApiResponse(responseCode = "500", description = "An internal server error was thrown"), 57 | }) 58 | CategoryResponse getById(@PathVariable(name = "id") String id); 59 | 60 | @PutMapping( 61 | value = "{id}", 62 | consumes = MediaType.APPLICATION_JSON_VALUE, 63 | produces = MediaType.APPLICATION_JSON_VALUE 64 | ) 65 | @Operation(summary = "Update a category by it's identifier") 66 | @ApiResponses(value = { 67 | @ApiResponse(responseCode = "200", description = "Category updated successfully"), 68 | @ApiResponse(responseCode = "404", description = "Category was not found"), 69 | @ApiResponse(responseCode = "500", description = "An internal server error was thrown"), 70 | }) 71 | ResponseEntity updateById(@PathVariable(name = "id") String id, @RequestBody UpdateCategoryRequest input); 72 | 73 | @DeleteMapping( 74 | value = "{id}", 75 | produces = MediaType.APPLICATION_JSON_VALUE 76 | ) 77 | @ResponseStatus(HttpStatus.NO_CONTENT) 78 | @Operation(summary = "Delete a category by it's identifier") 79 | @ApiResponses(value = { 80 | @ApiResponse(responseCode = "204", description = "Category deleted successfully"), 81 | @ApiResponse(responseCode = "404", description = "Category was not found"), 82 | @ApiResponse(responseCode = "500", description = "An internal server error was thrown"), 83 | }) 84 | void deleteById(@PathVariable(name = "id") String id); 85 | } 86 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/category/Category.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.category; 2 | 3 | import com.fullcycle.admin.catalogo.domain.AggregateRoot; 4 | import com.fullcycle.admin.catalogo.domain.validation.ValidationHandler; 5 | 6 | import java.time.Instant; 7 | import java.util.Objects; 8 | 9 | public class Category extends AggregateRoot implements Cloneable { 10 | private String name; 11 | private String description; 12 | private boolean active; 13 | private Instant createdAt; 14 | private Instant updatedAt; 15 | private Instant deletedAt; 16 | 17 | private Category( 18 | final CategoryID anId, 19 | final String aName, 20 | final String aDescription, 21 | final boolean isActive, 22 | final Instant aCreationDate, 23 | final Instant aUpdateDate, 24 | final Instant aDeleteDate 25 | ) { 26 | super(anId); 27 | this.name = aName; 28 | this.description = aDescription; 29 | this.active = isActive; 30 | this.createdAt = Objects.requireNonNull(aCreationDate, "'createdAt' should not be null"); 31 | this.updatedAt = Objects.requireNonNull(aUpdateDate, "'updatedAt' should not be null"); 32 | this.deletedAt = aDeleteDate; 33 | } 34 | 35 | public static Category newCategory(final String aName, final String aDescription, final boolean isActive) { 36 | final var id = CategoryID.unique(); 37 | final var now = Instant.now(); 38 | final var deletedAt = isActive ? null : now; 39 | return new Category(id, aName, aDescription, isActive, now, now, deletedAt); 40 | } 41 | 42 | public static Category with( 43 | final CategoryID anId, 44 | final String name, 45 | final String description, 46 | final boolean active, 47 | final Instant createdAt, 48 | final Instant updatedAt, 49 | final Instant deletedAt 50 | ) { 51 | return new Category( 52 | anId, 53 | name, 54 | description, 55 | active, 56 | createdAt, 57 | updatedAt, 58 | deletedAt 59 | ); 60 | } 61 | 62 | public static Category with(final Category aCategory) { 63 | return with( 64 | aCategory.getId(), 65 | aCategory.name, 66 | aCategory.description, 67 | aCategory.isActive(), 68 | aCategory.createdAt, 69 | aCategory.updatedAt, 70 | aCategory.deletedAt 71 | ); 72 | } 73 | 74 | @Override 75 | public void validate(final ValidationHandler handler) { 76 | new CategoryValidator(this, handler).validate(); 77 | } 78 | 79 | public Category activate() { 80 | this.deletedAt = null; 81 | this.active = true; 82 | this.updatedAt = Instant.now(); 83 | return this; 84 | } 85 | 86 | public Category deactivate() { 87 | if (getDeletedAt() == null) { 88 | this.deletedAt = Instant.now(); 89 | } 90 | 91 | this.active = false; 92 | this.updatedAt = Instant.now(); 93 | return this; 94 | } 95 | 96 | public Category update( 97 | final String aName, 98 | final String aDescription, 99 | final boolean isActive 100 | ) { 101 | if (isActive) { 102 | activate(); 103 | } else { 104 | deactivate(); 105 | } 106 | this.name = aName; 107 | this.description = aDescription; 108 | this.updatedAt = Instant.now(); 109 | return this; 110 | } 111 | 112 | public CategoryID getId() { 113 | return id; 114 | } 115 | 116 | public String getName() { 117 | return name; 118 | } 119 | 120 | public String getDescription() { 121 | return description; 122 | } 123 | 124 | public boolean isActive() { 125 | return active; 126 | } 127 | 128 | public Instant getCreatedAt() { 129 | return createdAt; 130 | } 131 | 132 | public Instant getUpdatedAt() { 133 | return updatedAt; 134 | } 135 | 136 | public Instant getDeletedAt() { 137 | return deletedAt; 138 | } 139 | 140 | @Override 141 | public Category clone() { 142 | try { 143 | return (Category) super.clone(); 144 | } catch (CloneNotSupportedException e) { 145 | throw new AssertionError(); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /application/src/test/java/com/fullcycle/admin/catalogo/application/category/retrieve/list/ListCategoriesUseCaseTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.retrieve.list; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCaseTest; 4 | import com.fullcycle.admin.catalogo.domain.category.Category; 5 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 6 | import com.fullcycle.admin.catalogo.domain.pagination.Pagination; 7 | import com.fullcycle.admin.catalogo.domain.pagination.SearchQuery; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.Test; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | 13 | import java.util.List; 14 | 15 | import static org.mockito.ArgumentMatchers.eq; 16 | import static org.mockito.Mockito.when; 17 | 18 | public class ListCategoriesUseCaseTest extends UseCaseTest { 19 | 20 | @InjectMocks 21 | private DefaultListCategoriesUseCase useCase; 22 | 23 | @Mock 24 | private CategoryGateway categoryGateway; 25 | 26 | @Override 27 | protected List getMocks() { 28 | return List.of(categoryGateway); 29 | } 30 | 31 | @Test 32 | public void givenAValidQuery_whenCallsListCategories_thenShouldReturnCategories() { 33 | final var categories = List.of( 34 | Category.newCategory("Filmes", null, true), 35 | Category.newCategory("Series", null, true) 36 | ); 37 | 38 | final var expectedPage = 0; 39 | final var expectedPerPage = 10; 40 | final var expectedTerms = ""; 41 | final var expectedSort = "createdAt"; 42 | final var expectedDirection = "asc"; 43 | 44 | final var aQuery = 45 | new SearchQuery(expectedPage, expectedPerPage, expectedTerms, expectedSort, expectedDirection); 46 | 47 | final var expectedPagination = 48 | new Pagination<>(expectedPage, expectedPerPage, categories.size(), categories); 49 | 50 | final var expectedItemsCount = 2; 51 | final var expectedResult = expectedPagination.map(CategoryListOutput::from); 52 | 53 | when(categoryGateway.findAll(eq(aQuery))) 54 | .thenReturn(expectedPagination); 55 | 56 | final var actualResult = useCase.execute(aQuery); 57 | 58 | Assertions.assertEquals(expectedItemsCount, actualResult.items().size()); 59 | Assertions.assertEquals(expectedResult, actualResult); 60 | Assertions.assertEquals(expectedPage, actualResult.currentPage()); 61 | Assertions.assertEquals(expectedPerPage, actualResult.perPage()); 62 | Assertions.assertEquals(categories.size(), actualResult.total()); 63 | } 64 | 65 | @Test 66 | public void givenAValidQuery_whenHasNoResults_thenShouldReturnEmptyCategories() { 67 | final var categories = List.of(); 68 | 69 | final var expectedPage = 0; 70 | final var expectedPerPage = 10; 71 | final var expectedTerms = ""; 72 | final var expectedSort = "createdAt"; 73 | final var expectedDirection = "asc"; 74 | 75 | final var aQuery = 76 | new SearchQuery(expectedPage, expectedPerPage, expectedTerms, expectedSort, expectedDirection); 77 | 78 | final var expectedPagination = 79 | new Pagination<>(expectedPage, expectedPerPage, categories.size(), categories); 80 | 81 | final var expectedItemsCount = 0; 82 | final var expectedResult = expectedPagination.map(CategoryListOutput::from); 83 | 84 | when(categoryGateway.findAll(eq(aQuery))) 85 | .thenReturn(expectedPagination); 86 | 87 | final var actualResult = useCase.execute(aQuery); 88 | 89 | Assertions.assertEquals(expectedItemsCount, actualResult.items().size()); 90 | Assertions.assertEquals(expectedResult, actualResult); 91 | Assertions.assertEquals(expectedPage, actualResult.currentPage()); 92 | Assertions.assertEquals(expectedPerPage, actualResult.perPage()); 93 | Assertions.assertEquals(categories.size(), actualResult.total()); 94 | } 95 | 96 | @Test 97 | public void givenAValidQuery_whenGatewayThrowsException_shouldReturnException() { 98 | final var expectedPage = 0; 99 | final var expectedPerPage = 10; 100 | final var expectedTerms = ""; 101 | final var expectedSort = "createdAt"; 102 | final var expectedDirection = "asc"; 103 | final var expectedErrorMessage = "Gateway error"; 104 | 105 | final var aQuery = 106 | new SearchQuery(expectedPage, expectedPerPage, expectedTerms, expectedSort, expectedDirection); 107 | 108 | when(categoryGateway.findAll(eq(aQuery))) 109 | .thenThrow(new IllegalStateException(expectedErrorMessage)); 110 | 111 | final var actualException = 112 | Assertions.assertThrows(IllegalStateException.class, () -> useCase.execute(aQuery)); 113 | 114 | Assertions.assertEquals(expectedErrorMessage, actualException.getMessage()); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/genre/persistence/GenreJpaEntity.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.genre.persistence; 2 | 3 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 4 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 5 | import com.fullcycle.admin.catalogo.domain.genre.GenreID; 6 | 7 | import javax.persistence.*; 8 | import java.time.Instant; 9 | import java.util.HashSet; 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | import static javax.persistence.CascadeType.ALL; 14 | import static javax.persistence.FetchType.EAGER; 15 | 16 | @Entity 17 | @Table(name = "genres") 18 | public class GenreJpaEntity { 19 | 20 | @Id 21 | @Column(name = "id", nullable = false) 22 | private String id; 23 | 24 | @Column(name = "name", nullable = false) 25 | private String name; 26 | 27 | @Column(name = "active", nullable = false) 28 | private boolean active; 29 | 30 | @OneToMany(mappedBy = "genre", cascade = ALL, fetch = EAGER, orphanRemoval = true) 31 | private Set categories; 32 | 33 | @Column(name = "created_at", nullable = false, columnDefinition = "DATETIME(6)") 34 | private Instant createdAt; 35 | 36 | @Column(name = "updated_at", nullable = false, columnDefinition = "DATETIME(6)") 37 | private Instant updatedAt; 38 | 39 | @Column(name = "deleted_at", columnDefinition = "DATETIME(6)") 40 | private Instant deletedAt; 41 | 42 | public GenreJpaEntity() { 43 | } 44 | 45 | private GenreJpaEntity( 46 | final String anId, 47 | final String aName, 48 | final boolean isActive, 49 | final Instant createdAt, 50 | final Instant updatedAt, 51 | final Instant deletedAt 52 | ) { 53 | this.id = anId; 54 | this.name = aName; 55 | this.active = isActive; 56 | this.categories = new HashSet<>(); 57 | this.createdAt = createdAt; 58 | this.updatedAt = updatedAt; 59 | this.deletedAt = deletedAt; 60 | } 61 | 62 | public static GenreJpaEntity from(final Genre aGenre) { 63 | final var anEntity = new GenreJpaEntity( 64 | aGenre.getId().getValue(), 65 | aGenre.getName(), 66 | aGenre.isActive(), 67 | aGenre.getCreatedAt(), 68 | aGenre.getUpdatedAt(), 69 | aGenre.getDeletedAt() 70 | ); 71 | 72 | aGenre.getCategories() 73 | .forEach(anEntity::addCategory); 74 | 75 | return anEntity; 76 | } 77 | 78 | public Genre toAggregate() { 79 | return Genre.with( 80 | GenreID.from(getId()), 81 | getName(), 82 | isActive(), 83 | getCategoryIDs(), 84 | getCreatedAt(), 85 | getUpdatedAt(), 86 | getDeletedAt() 87 | ); 88 | } 89 | 90 | private void addCategory(final CategoryID anId) { 91 | this.categories.add(GenreCategoryJpaEntity.from(this, anId)); 92 | } 93 | 94 | private void removeCategory(final CategoryID anId) { 95 | this.categories.remove(GenreCategoryJpaEntity.from(this, anId)); 96 | } 97 | 98 | public String getId() { 99 | return id; 100 | } 101 | 102 | public GenreJpaEntity setId(String id) { 103 | this.id = id; 104 | return this; 105 | } 106 | 107 | public String getName() { 108 | return name; 109 | } 110 | 111 | public GenreJpaEntity setName(String name) { 112 | this.name = name; 113 | return this; 114 | } 115 | 116 | public boolean isActive() { 117 | return active; 118 | } 119 | 120 | public GenreJpaEntity setActive(boolean active) { 121 | this.active = active; 122 | return this; 123 | } 124 | 125 | public List getCategoryIDs() { 126 | return getCategories().stream() 127 | .map(it -> CategoryID.from(it.getId().getCategoryId())) 128 | .toList(); 129 | } 130 | 131 | public Set getCategories() { 132 | return categories; 133 | } 134 | 135 | public GenreJpaEntity setCategories(Set categories) { 136 | this.categories = categories; 137 | return this; 138 | } 139 | 140 | public Instant getCreatedAt() { 141 | return createdAt; 142 | } 143 | 144 | public GenreJpaEntity setCreatedAt(Instant createdAt) { 145 | this.createdAt = createdAt; 146 | return this; 147 | } 148 | 149 | public Instant getUpdatedAt() { 150 | return updatedAt; 151 | } 152 | 153 | public GenreJpaEntity setUpdatedAt(Instant updatedAt) { 154 | this.updatedAt = updatedAt; 155 | return this; 156 | } 157 | 158 | public Instant getDeletedAt() { 159 | return deletedAt; 160 | } 161 | 162 | public GenreJpaEntity setDeletedAt(Instant deletedAt) { 163 | this.deletedAt = deletedAt; 164 | return this; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /application/src/test/java/com/fullcycle/admin/catalogo/application/genre/retrieve/list/ListGenreUseCaseTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.genre.retrieve.list; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCaseTest; 4 | import com.fullcycle.admin.catalogo.domain.genre.Genre; 5 | import com.fullcycle.admin.catalogo.domain.genre.GenreGateway; 6 | import com.fullcycle.admin.catalogo.domain.pagination.Pagination; 7 | import com.fullcycle.admin.catalogo.domain.pagination.SearchQuery; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.Test; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.Mockito; 13 | 14 | import java.util.List; 15 | 16 | import static org.mockito.ArgumentMatchers.any; 17 | import static org.mockito.ArgumentMatchers.eq; 18 | import static org.mockito.Mockito.times; 19 | import static org.mockito.Mockito.when; 20 | 21 | public class ListGenreUseCaseTest extends UseCaseTest { 22 | 23 | @InjectMocks 24 | private DefaultListGenreUseCase useCase; 25 | 26 | @Mock 27 | private GenreGateway genreGateway; 28 | 29 | @Override 30 | protected List getMocks() { 31 | return List.of(genreGateway); 32 | } 33 | 34 | @Test 35 | public void givenAValidQuery_whenCallsListGenre_shouldReturnGenres() { 36 | // given 37 | final var genres = List.of( 38 | Genre.newGenre("Ação", true), 39 | Genre.newGenre("Aventura", true) 40 | ); 41 | 42 | final var expectedPage = 0; 43 | final var expectedPerPage = 10; 44 | final var expectedTerms = "A"; 45 | final var expectedSort = "createdAt"; 46 | final var expectedDirection = "asc"; 47 | final var expectedTotal = 2; 48 | 49 | final var expectedItems = genres.stream() 50 | .map(GenreListOutput::from) 51 | .toList(); 52 | 53 | final var expectedPagination = new Pagination<>( 54 | expectedPage, 55 | expectedPerPage, 56 | expectedTotal, 57 | genres 58 | ); 59 | 60 | when(genreGateway.findAll(any())) 61 | .thenReturn(expectedPagination); 62 | 63 | final var aQuery = 64 | new SearchQuery(expectedPage, expectedPerPage, expectedTerms, expectedSort, expectedDirection); 65 | 66 | // when 67 | final var actualOutput = useCase.execute(aQuery); 68 | 69 | // then 70 | Assertions.assertEquals(expectedPage, actualOutput.currentPage()); 71 | Assertions.assertEquals(expectedPerPage, actualOutput.perPage()); 72 | Assertions.assertEquals(expectedTotal, actualOutput.total()); 73 | Assertions.assertEquals(expectedItems, actualOutput.items()); 74 | 75 | Mockito.verify(genreGateway, times(1)).findAll(eq(aQuery)); 76 | } 77 | 78 | @Test 79 | public void givenAValidQuery_whenCallsListGenreAndResultIsEmpty_shouldReturnGenres() { 80 | // given 81 | final var genres = List.of(); 82 | 83 | final var expectedPage = 0; 84 | final var expectedPerPage = 10; 85 | final var expectedTerms = "A"; 86 | final var expectedSort = "createdAt"; 87 | final var expectedDirection = "asc"; 88 | final var expectedTotal = 0; 89 | 90 | final var expectedItems = List.of(); 91 | 92 | final var expectedPagination = new Pagination<>( 93 | expectedPage, 94 | expectedPerPage, 95 | expectedTotal, 96 | genres 97 | ); 98 | 99 | when(genreGateway.findAll(any())) 100 | .thenReturn(expectedPagination); 101 | 102 | final var aQuery = 103 | new SearchQuery(expectedPage, expectedPerPage, expectedTerms, expectedSort, expectedDirection); 104 | 105 | // when 106 | final var actualOutput = useCase.execute(aQuery); 107 | 108 | // then 109 | Assertions.assertEquals(expectedPage, actualOutput.currentPage()); 110 | Assertions.assertEquals(expectedPerPage, actualOutput.perPage()); 111 | Assertions.assertEquals(expectedTotal, actualOutput.total()); 112 | Assertions.assertEquals(expectedItems, actualOutput.items()); 113 | 114 | Mockito.verify(genreGateway, times(1)).findAll(eq(aQuery)); 115 | } 116 | 117 | @Test 118 | public void givenAValidQuery_whenCallsListGenreAndGatewayThrowsRandomError_shouldReturnException() { 119 | // given 120 | final var expectedPage = 0; 121 | final var expectedPerPage = 10; 122 | final var expectedTerms = "A"; 123 | final var expectedSort = "createdAt"; 124 | final var expectedDirection = "asc"; 125 | 126 | final var expectedErrorMessage = "Gateway error"; 127 | 128 | when(genreGateway.findAll(any())) 129 | .thenThrow(new IllegalStateException(expectedErrorMessage)); 130 | 131 | final var aQuery = 132 | new SearchQuery(expectedPage, expectedPerPage, expectedTerms, expectedSort, expectedDirection); 133 | 134 | // when 135 | final var actualOutput = Assertions.assertThrows( 136 | IllegalStateException.class, 137 | () -> useCase.execute(aQuery) 138 | ); 139 | 140 | // then 141 | Assertions.assertEquals(expectedErrorMessage, actualOutput.getMessage()); 142 | 143 | Mockito.verify(genreGateway, times(1)).findAll(eq(aQuery)); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/fullcycle/admin/catalogo/infrastructure/api/controllers/CategoryController.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.infrastructure.api.controllers; 2 | 3 | import com.fullcycle.admin.catalogo.application.category.create.CreateCategoryCommand; 4 | import com.fullcycle.admin.catalogo.application.category.create.CreateCategoryOutput; 5 | import com.fullcycle.admin.catalogo.application.category.create.CreateCategoryUseCase; 6 | import com.fullcycle.admin.catalogo.application.category.delete.DeleteCategoryUseCase; 7 | import com.fullcycle.admin.catalogo.application.category.retrieve.get.GetCategoryByIdUseCase; 8 | import com.fullcycle.admin.catalogo.application.category.retrieve.list.ListCategoriesUseCase; 9 | import com.fullcycle.admin.catalogo.application.category.update.UpdateCategoryCommand; 10 | import com.fullcycle.admin.catalogo.application.category.update.UpdateCategoryOutput; 11 | import com.fullcycle.admin.catalogo.application.category.update.UpdateCategoryUseCase; 12 | import com.fullcycle.admin.catalogo.domain.pagination.SearchQuery; 13 | import com.fullcycle.admin.catalogo.domain.pagination.Pagination; 14 | import com.fullcycle.admin.catalogo.domain.validation.handler.Notification; 15 | import com.fullcycle.admin.catalogo.infrastructure.api.CategoryAPI; 16 | import com.fullcycle.admin.catalogo.infrastructure.category.models.CategoryListResponse; 17 | import com.fullcycle.admin.catalogo.infrastructure.category.models.CategoryResponse; 18 | import com.fullcycle.admin.catalogo.infrastructure.category.models.CreateCategoryRequest; 19 | import com.fullcycle.admin.catalogo.infrastructure.category.models.UpdateCategoryRequest; 20 | import com.fullcycle.admin.catalogo.infrastructure.category.presenters.CategoryApiPresenter; 21 | import org.springframework.http.ResponseEntity; 22 | import org.springframework.web.bind.annotation.RestController; 23 | 24 | import java.net.URI; 25 | import java.util.Objects; 26 | import java.util.function.Function; 27 | 28 | @RestController 29 | public class CategoryController implements CategoryAPI { 30 | 31 | private final CreateCategoryUseCase createCategoryUseCase; 32 | private final GetCategoryByIdUseCase getCategoryByIdUseCase; 33 | private final UpdateCategoryUseCase updateCategoryUseCase; 34 | private final DeleteCategoryUseCase deleteCategoryUseCase; 35 | private final ListCategoriesUseCase listCategoriesUseCase; 36 | 37 | public CategoryController( 38 | final CreateCategoryUseCase createCategoryUseCase, 39 | final GetCategoryByIdUseCase getCategoryByIdUseCase, 40 | final UpdateCategoryUseCase updateCategoryUseCase, 41 | final DeleteCategoryUseCase deleteCategoryUseCase, 42 | final ListCategoriesUseCase listCategoriesUseCase 43 | ) { 44 | this.createCategoryUseCase = Objects.requireNonNull(createCategoryUseCase); 45 | this.getCategoryByIdUseCase = Objects.requireNonNull(getCategoryByIdUseCase); 46 | this.updateCategoryUseCase = Objects.requireNonNull(updateCategoryUseCase); 47 | this.deleteCategoryUseCase = Objects.requireNonNull(deleteCategoryUseCase); 48 | this.listCategoriesUseCase = Objects.requireNonNull(listCategoriesUseCase); 49 | } 50 | 51 | @Override 52 | public ResponseEntity createCategory(final CreateCategoryRequest input) { 53 | final var aCommand = CreateCategoryCommand.with( 54 | input.name(), 55 | input.description(), 56 | input.active() != null ? input.active() : true 57 | ); 58 | 59 | final Function> onError = notification -> 60 | ResponseEntity.unprocessableEntity().body(notification); 61 | 62 | final Function> onSuccess = output -> 63 | ResponseEntity.created(URI.create("/categories/" + output.id())).body(output); 64 | 65 | return this.createCategoryUseCase.execute(aCommand) 66 | .fold(onError, onSuccess); 67 | } 68 | 69 | @Override 70 | public Pagination listCategories( 71 | final String search, 72 | final int page, 73 | final int perPage, 74 | final String sort, 75 | final String direction 76 | ) { 77 | return listCategoriesUseCase.execute(new SearchQuery(page, perPage, search, sort, direction)) 78 | .map(CategoryApiPresenter::present); 79 | } 80 | 81 | @Override 82 | public CategoryResponse getById(final String id) { 83 | return CategoryApiPresenter.present(this.getCategoryByIdUseCase.execute(id)); 84 | } 85 | 86 | @Override 87 | public ResponseEntity updateById(final String id, final UpdateCategoryRequest input) { 88 | final var aCommand = UpdateCategoryCommand.with( 89 | id, 90 | input.name(), 91 | input.description(), 92 | input.active() != null ? input.active() : true 93 | ); 94 | 95 | final Function> onError = notification -> 96 | ResponseEntity.unprocessableEntity().body(notification); 97 | 98 | final Function> onSuccess = 99 | ResponseEntity::ok; 100 | 101 | return this.updateCategoryUseCase.execute(aCommand) 102 | .fold(onError, onSuccess); 103 | } 104 | 105 | @Override 106 | public void deleteById(final String anId) { 107 | this.deleteCategoryUseCase.execute(anId); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /domain/src/main/java/com/fullcycle/admin/catalogo/domain/genre/Genre.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.domain.genre; 2 | 3 | import com.fullcycle.admin.catalogo.domain.AggregateRoot; 4 | import com.fullcycle.admin.catalogo.domain.category.CategoryID; 5 | import com.fullcycle.admin.catalogo.domain.exceptions.NotificationException; 6 | import com.fullcycle.admin.catalogo.domain.utils.InstantUtils; 7 | import com.fullcycle.admin.catalogo.domain.validation.ValidationHandler; 8 | import com.fullcycle.admin.catalogo.domain.validation.handler.Notification; 9 | 10 | import java.time.Instant; 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | public class Genre extends AggregateRoot { 16 | 17 | private String name; 18 | private boolean active; 19 | private List categories; 20 | private Instant createdAt; 21 | private Instant updatedAt; 22 | private Instant deletedAt; 23 | 24 | protected Genre( 25 | final GenreID anId, 26 | final String aName, 27 | final boolean isActive, 28 | final List categories, 29 | final Instant aCreatedAt, 30 | final Instant aUpdatedAt, 31 | final Instant aDeletedAt 32 | ) { 33 | super(anId); 34 | this.name = aName; 35 | this.categories = categories; 36 | this.active = isActive; 37 | this.createdAt = aCreatedAt; 38 | this.updatedAt = aUpdatedAt; 39 | this.deletedAt = aDeletedAt; 40 | selfValidate(); 41 | } 42 | 43 | public static Genre newGenre(final String aName, final boolean isActive) { 44 | final var anId = GenreID.unique(); 45 | final var now = InstantUtils.now(); 46 | final var deletedAt = isActive ? null : now; 47 | return new Genre(anId, aName, isActive, new ArrayList<>(), now, now, deletedAt); 48 | } 49 | 50 | public static Genre with( 51 | final GenreID anId, 52 | final String aName, 53 | final boolean isActive, 54 | final List categories, 55 | final Instant aCreatedAt, 56 | final Instant aUpdatedAt, 57 | final Instant aDeletedAt 58 | ) { 59 | return new Genre(anId, aName, isActive, categories, aCreatedAt, aUpdatedAt, aDeletedAt); 60 | } 61 | 62 | public static Genre with(final Genre aGenre) { 63 | return new Genre( 64 | aGenre.id, 65 | aGenre.name, 66 | aGenre.active, 67 | new ArrayList<>(aGenre.categories), 68 | aGenre.createdAt, 69 | aGenre.updatedAt, 70 | aGenre.deletedAt 71 | ); 72 | } 73 | 74 | @Override 75 | public void validate(final ValidationHandler handler) { 76 | new GenreValidator(this, handler).validate(); 77 | } 78 | 79 | public Genre update(final String aName, final boolean isActive, final List categories) { 80 | if (isActive) { 81 | activate(); 82 | } else { 83 | deactivate(); 84 | } 85 | this.name = aName; 86 | this.categories = new ArrayList<>(categories != null ? categories : Collections.emptyList()); 87 | this.updatedAt = InstantUtils.now(); 88 | selfValidate(); 89 | return this; 90 | } 91 | 92 | public Genre deactivate() { 93 | if (getDeletedAt() == null) { 94 | this.deletedAt = InstantUtils.now(); 95 | } 96 | this.active = false; 97 | this.updatedAt = InstantUtils.now(); 98 | return this; 99 | } 100 | 101 | public Genre activate() { 102 | this.deletedAt = null; 103 | this.active = true; 104 | this.updatedAt = InstantUtils.now(); 105 | return this; 106 | } 107 | 108 | public String getName() { 109 | return name; 110 | } 111 | 112 | public boolean isActive() { 113 | return active; 114 | } 115 | 116 | public List getCategories() { 117 | return Collections.unmodifiableList(categories); 118 | } 119 | 120 | public Instant getCreatedAt() { 121 | return createdAt; 122 | } 123 | 124 | public Instant getUpdatedAt() { 125 | return updatedAt; 126 | } 127 | 128 | public Instant getDeletedAt() { 129 | return deletedAt; 130 | } 131 | 132 | private void selfValidate() { 133 | final var notification = Notification.create(); 134 | validate(notification); 135 | 136 | if (notification.hasError()) { 137 | throw new NotificationException("Failed to create a Aggregate Genre", notification); 138 | } 139 | } 140 | 141 | public Genre addCategory(final CategoryID aCategoryID) { 142 | if (aCategoryID == null) { 143 | return this; 144 | } 145 | this.categories.add(aCategoryID); 146 | this.updatedAt = InstantUtils.now(); 147 | return this; 148 | } 149 | 150 | public Genre addCategories(final List categories) { 151 | if (categories == null || categories.isEmpty()) { 152 | return this; 153 | } 154 | this.categories.addAll(categories); 155 | this.updatedAt = InstantUtils.now(); 156 | return this; 157 | } 158 | 159 | public Genre removeCategory(final CategoryID aCategoryID) { 160 | if (aCategoryID == null) { 161 | return this; 162 | } 163 | this.categories.remove(aCategoryID); 164 | this.updatedAt = InstantUtils.now(); 165 | return this; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/application/category/create/CreateCategoryUseCaseIT.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.create; 2 | 3 | import com.fullcycle.admin.catalogo.IntegrationTest; 4 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 5 | import com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryRepository; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | import org.mockito.Mockito; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.mock.mockito.SpyBean; 11 | 12 | import static org.mockito.ArgumentMatchers.any; 13 | import static org.mockito.Mockito.doThrow; 14 | import static org.mockito.Mockito.times; 15 | 16 | @IntegrationTest 17 | public class CreateCategoryUseCaseIT { 18 | 19 | @Autowired 20 | private CreateCategoryUseCase useCase; 21 | 22 | @Autowired 23 | private CategoryRepository categoryRepository; 24 | 25 | @SpyBean 26 | private CategoryGateway categoryGateway; 27 | 28 | @Test 29 | public void givenAValidCommand_whenCallsCreateCategory_shouldReturnCategoryId() { 30 | final var expectedName = "Filmes"; 31 | final var expectedDescription = "A categoria mais assistida"; 32 | final var expectedIsActive = true; 33 | 34 | Assertions.assertEquals(0, categoryRepository.count()); 35 | 36 | final var aCommand = 37 | CreateCategoryCommand.with(expectedName, expectedDescription, expectedIsActive); 38 | 39 | final var actualOutput = useCase.execute(aCommand).get(); 40 | 41 | Assertions.assertNotNull(actualOutput); 42 | Assertions.assertNotNull(actualOutput.id()); 43 | 44 | Assertions.assertEquals(1, categoryRepository.count()); 45 | 46 | final var actualCategory = 47 | categoryRepository.findById(actualOutput.id()).get(); 48 | 49 | Assertions.assertEquals(expectedName, actualCategory.getName()); 50 | Assertions.assertEquals(expectedDescription, actualCategory.getDescription()); 51 | Assertions.assertEquals(expectedIsActive, actualCategory.isActive()); 52 | Assertions.assertNotNull(actualCategory.getCreatedAt()); 53 | Assertions.assertNotNull(actualCategory.getUpdatedAt()); 54 | Assertions.assertNull(actualCategory.getDeletedAt()); 55 | } 56 | 57 | @Test 58 | public void givenAInvalidName_whenCallsCreateCategory_thenShouldReturnDomainException() { 59 | final String expectedName = null; 60 | final var expectedDescription = "A categoria mais assistida"; 61 | final var expectedIsActive = true; 62 | final var expectedErrorMessage = "'name' should not be null"; 63 | final var expectedErrorCount = 1; 64 | 65 | Assertions.assertEquals(0, categoryRepository.count()); 66 | 67 | final var aCommand = 68 | CreateCategoryCommand.with(expectedName, expectedDescription, expectedIsActive); 69 | 70 | final var notification = useCase.execute(aCommand).getLeft(); 71 | 72 | Assertions.assertEquals(expectedErrorCount, notification.getErrors().size()); 73 | Assertions.assertEquals(expectedErrorMessage, notification.firstError().message()); 74 | 75 | Assertions.assertEquals(0, categoryRepository.count()); 76 | 77 | Mockito.verify(categoryGateway, times(0)).create(any()); 78 | } 79 | 80 | @Test 81 | public void givenAValidCommandWithInactiveCategory_whenCallsCreateCategory_shouldReturnInactiveCategoryId() { 82 | final var expectedName = "Filmes"; 83 | final var expectedDescription = "A categoria mais assistida"; 84 | final var expectedIsActive = false; 85 | 86 | Assertions.assertEquals(0, categoryRepository.count()); 87 | 88 | final var aCommand = 89 | CreateCategoryCommand.with(expectedName, expectedDescription, expectedIsActive); 90 | 91 | final var actualOutput = useCase.execute(aCommand).get(); 92 | 93 | Assertions.assertNotNull(actualOutput); 94 | Assertions.assertNotNull(actualOutput.id()); 95 | 96 | Assertions.assertEquals(1, categoryRepository.count()); 97 | 98 | final var actualCategory = 99 | categoryRepository.findById(actualOutput.id()).get(); 100 | 101 | Assertions.assertEquals(expectedName, actualCategory.getName()); 102 | Assertions.assertEquals(expectedDescription, actualCategory.getDescription()); 103 | Assertions.assertEquals(expectedIsActive, actualCategory.isActive()); 104 | Assertions.assertNotNull(actualCategory.getCreatedAt()); 105 | Assertions.assertNotNull(actualCategory.getUpdatedAt()); 106 | Assertions.assertNotNull(actualCategory.getDeletedAt()); 107 | } 108 | 109 | @Test 110 | public void givenAValidCommand_whenGatewayThrowsRandomException_shouldReturnAException() { 111 | final var expectedName = "Filmes"; 112 | final var expectedDescription = "A categoria mais assistida"; 113 | final var expectedIsActive = true; 114 | final var expectedErrorCount = 1; 115 | final var expectedErrorMessage = "Gateway error"; 116 | 117 | final var aCommand = 118 | CreateCategoryCommand.with(expectedName, expectedDescription, expectedIsActive); 119 | 120 | doThrow(new IllegalStateException(expectedErrorMessage)) 121 | .when(categoryGateway).create(any()); 122 | 123 | final var notification = useCase.execute(aCommand).getLeft(); 124 | 125 | Assertions.assertEquals(expectedErrorCount, notification.getErrors().size()); 126 | Assertions.assertEquals(expectedErrorMessage, notification.firstError().message()); 127 | } 128 | } -------------------------------------------------------------------------------- /application/src/test/java/com/fullcycle/admin/catalogo/application/category/create/CreateCategoryUseCaseTest.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.create; 2 | 3 | import com.fullcycle.admin.catalogo.application.UseCaseTest; 4 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | import org.mockito.InjectMocks; 8 | import org.mockito.Mock; 9 | import org.mockito.Mockito; 10 | 11 | import java.util.List; 12 | import java.util.Objects; 13 | 14 | import static org.mockito.AdditionalAnswers.returnsFirstArg; 15 | import static org.mockito.ArgumentMatchers.any; 16 | import static org.mockito.ArgumentMatchers.argThat; 17 | import static org.mockito.Mockito.times; 18 | import static org.mockito.Mockito.when; 19 | 20 | public class CreateCategoryUseCaseTest extends UseCaseTest { 21 | 22 | @InjectMocks 23 | private DefaultCreateCategoryUseCase useCase; 24 | 25 | @Mock 26 | private CategoryGateway categoryGateway; 27 | 28 | @Override 29 | protected List getMocks() { 30 | return List.of(categoryGateway); 31 | } 32 | 33 | // 1. Teste do caminho feliz 34 | // 2. Teste passando uma propriedade inválida (name) 35 | // 3. Teste criando uma categoria inativa 36 | // 4. Teste simulando um erro generico vindo do gateway 37 | 38 | @Test 39 | public void givenAValidCommand_whenCallsCreateCategory_shouldReturnCategoryId() { 40 | final var expectedName = "Filmes"; 41 | final var expectedDescription = "A categoria mais assistida"; 42 | final var expectedIsActive = true; 43 | 44 | final var aCommand = 45 | CreateCategoryCommand.with(expectedName, expectedDescription, expectedIsActive); 46 | 47 | when(categoryGateway.create(any())) 48 | .thenAnswer(returnsFirstArg()); 49 | 50 | final var actualOutput = useCase.execute(aCommand).get(); 51 | 52 | Assertions.assertNotNull(actualOutput); 53 | Assertions.assertNotNull(actualOutput.id()); 54 | 55 | Mockito.verify(categoryGateway, times(1)).create(argThat(aCategory -> 56 | Objects.equals(expectedName, aCategory.getName()) 57 | && Objects.equals(expectedDescription, aCategory.getDescription()) 58 | && Objects.equals(expectedIsActive, aCategory.isActive()) 59 | && Objects.nonNull(aCategory.getId()) 60 | && Objects.nonNull(aCategory.getCreatedAt()) 61 | && Objects.nonNull(aCategory.getUpdatedAt()) 62 | && Objects.isNull(aCategory.getDeletedAt()) 63 | )); 64 | } 65 | 66 | @Test 67 | public void givenAInvalidName_whenCallsCreateCategory_thenShouldReturnDomainException() { 68 | final String expectedName = null; 69 | final var expectedDescription = "A categoria mais assistida"; 70 | final var expectedIsActive = true; 71 | final var expectedErrorMessage = "'name' should not be null"; 72 | final var expectedErrorCount = 1; 73 | 74 | final var aCommand = 75 | CreateCategoryCommand.with(expectedName, expectedDescription, expectedIsActive); 76 | 77 | final var notification = useCase.execute(aCommand).getLeft(); 78 | 79 | Assertions.assertEquals(expectedErrorCount, notification.getErrors().size()); 80 | Assertions.assertEquals(expectedErrorMessage, notification.firstError().message()); 81 | 82 | Mockito.verify(categoryGateway, times(0)).create(any()); 83 | } 84 | 85 | @Test 86 | public void givenAValidCommandWithInactiveCategory_whenCallsCreateCategory_shouldReturnInactiveCategoryId() { 87 | final var expectedName = "Filmes"; 88 | final var expectedDescription = "A categoria mais assistida"; 89 | final var expectedIsActive = false; 90 | 91 | final var aCommand = 92 | CreateCategoryCommand.with(expectedName, expectedDescription, expectedIsActive); 93 | 94 | when(categoryGateway.create(any())) 95 | .thenAnswer(returnsFirstArg()); 96 | 97 | final var actualOutput = useCase.execute(aCommand).get(); 98 | 99 | Assertions.assertNotNull(actualOutput); 100 | Assertions.assertNotNull(actualOutput.id()); 101 | 102 | Mockito.verify(categoryGateway, times(1)).create(argThat(aCategory -> 103 | Objects.equals(expectedName, aCategory.getName()) 104 | && Objects.equals(expectedDescription, aCategory.getDescription()) 105 | && Objects.equals(expectedIsActive, aCategory.isActive()) 106 | && Objects.nonNull(aCategory.getId()) 107 | && Objects.nonNull(aCategory.getCreatedAt()) 108 | && Objects.nonNull(aCategory.getUpdatedAt()) 109 | && Objects.nonNull(aCategory.getDeletedAt()) 110 | )); 111 | } 112 | 113 | @Test 114 | public void givenAValidCommand_whenGatewayThrowsRandomException_shouldReturnAException() { 115 | final var expectedName = "Filmes"; 116 | final var expectedDescription = "A categoria mais assistida"; 117 | final var expectedIsActive = true; 118 | final var expectedErrorCount = 1; 119 | final var expectedErrorMessage = "Gateway error"; 120 | 121 | final var aCommand = 122 | CreateCategoryCommand.with(expectedName, expectedDescription, expectedIsActive); 123 | 124 | when(categoryGateway.create(any())) 125 | .thenThrow(new IllegalStateException(expectedErrorMessage)); 126 | 127 | final var notification = useCase.execute(aCommand).getLeft(); 128 | 129 | Assertions.assertEquals(expectedErrorCount, notification.getErrors().size()); 130 | Assertions.assertEquals(expectedErrorMessage, notification.firstError().message()); 131 | 132 | Mockito.verify(categoryGateway, times(1)).create(argThat(aCategory -> 133 | Objects.equals(expectedName, aCategory.getName()) 134 | && Objects.equals(expectedDescription, aCategory.getDescription()) 135 | && Objects.equals(expectedIsActive, aCategory.isActive()) 136 | && Objects.nonNull(aCategory.getId()) 137 | && Objects.nonNull(aCategory.getCreatedAt()) 138 | && Objects.nonNull(aCategory.getUpdatedAt()) 139 | && Objects.isNull(aCategory.getDeletedAt()) 140 | )); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/application/category/retrieve/list/ListCategoriesUseCaseIT.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.retrieve.list; 2 | 3 | import com.fullcycle.admin.catalogo.IntegrationTest; 4 | import com.fullcycle.admin.catalogo.domain.category.Category; 5 | import com.fullcycle.admin.catalogo.domain.pagination.SearchQuery; 6 | import com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryJpaEntity; 7 | import com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryRepository; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.params.ParameterizedTest; 12 | import org.junit.jupiter.params.provider.CsvSource; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | 15 | import java.util.stream.Stream; 16 | 17 | @IntegrationTest 18 | public class ListCategoriesUseCaseIT { 19 | 20 | @Autowired 21 | private ListCategoriesUseCase useCase; 22 | 23 | @Autowired 24 | private CategoryRepository categoryRepository; 25 | 26 | @BeforeEach 27 | void mockUp() { 28 | final var categories = Stream.of( 29 | Category.newCategory("Filmes", null, true), 30 | Category.newCategory("Netflix Originals", "Títulos de autoria da Netflix", true), 31 | Category.newCategory("Amazon Originals", "Títulos de autoria da Amazon Prime", true), 32 | Category.newCategory("Documentários", null, true), 33 | Category.newCategory("Sports", null, true), 34 | Category.newCategory("Kids", "Categoria para crianças", true), 35 | Category.newCategory("Series", null, true) 36 | ) 37 | .map(CategoryJpaEntity::from) 38 | .toList(); 39 | 40 | categoryRepository.saveAllAndFlush(categories); 41 | } 42 | 43 | @Test 44 | public void givenAValidTerm_whenTermDoesntMatchsPrePersisted_shouldReturnEmptyPage() { 45 | final var expectedPage = 0; 46 | final var expectedPerPage = 10; 47 | final var expectedTerms = "ji1j3i 1j3i1oj"; 48 | final var expectedSort = "name"; 49 | final var expectedDirection = "asc"; 50 | final var expectedItemsCount = 0; 51 | final var expectedTotal = 0; 52 | 53 | final var aQuery = 54 | new SearchQuery(expectedPage, expectedPerPage, expectedTerms, expectedSort, expectedDirection); 55 | 56 | final var actualResult = useCase.execute(aQuery); 57 | 58 | Assertions.assertEquals(expectedItemsCount, actualResult.items().size()); 59 | Assertions.assertEquals(expectedPage, actualResult.currentPage()); 60 | Assertions.assertEquals(expectedPerPage, actualResult.perPage()); 61 | Assertions.assertEquals(expectedTotal, actualResult.total()); 62 | } 63 | 64 | @ParameterizedTest 65 | @CsvSource({ 66 | "fil,0,10,1,1,Filmes", 67 | "net,0,10,1,1,Netflix Originals", 68 | "ZON,0,10,1,1,Amazon Originals", 69 | "KI,0,10,1,1,Kids", 70 | "crianças,0,10,1,1,Kids", 71 | "da Amazon,0,10,1,1,Amazon Originals", 72 | }) 73 | public void givenAValidTerm_whenCallsListCategories_shouldReturnCategoriesFiltered( 74 | final String expectedTerms, 75 | final int expectedPage, 76 | final int expectedPerPage, 77 | final int expectedItemsCount, 78 | final long expectedTotal, 79 | final String expectedCategoryName 80 | ) { 81 | final var expectedSort = "name"; 82 | final var expectedDirection = "asc"; 83 | 84 | final var aQuery = 85 | new SearchQuery(expectedPage, expectedPerPage, expectedTerms, expectedSort, expectedDirection); 86 | 87 | final var actualResult = useCase.execute(aQuery); 88 | 89 | Assertions.assertEquals(expectedItemsCount, actualResult.items().size()); 90 | Assertions.assertEquals(expectedPage, actualResult.currentPage()); 91 | Assertions.assertEquals(expectedPerPage, actualResult.perPage()); 92 | Assertions.assertEquals(expectedTotal, actualResult.total()); 93 | Assertions.assertEquals(expectedCategoryName, actualResult.items().get(0).name()); 94 | } 95 | 96 | @ParameterizedTest 97 | @CsvSource({ 98 | "name,asc,0,10,7,7,Amazon Originals", 99 | "name,desc,0,10,7,7,Sports", 100 | "createdAt,asc,0,10,7,7,Filmes", 101 | "createdAt,desc,0,10,7,7,Series", 102 | }) 103 | public void givenAValidSortAndDirection_whenCallsListCategories_thenShouldReturnCategoriesOrdered( 104 | final String expectedSort, 105 | final String expectedDirection, 106 | final int expectedPage, 107 | final int expectedPerPage, 108 | final int expectedItemsCount, 109 | final long expectedTotal, 110 | final String expectedCategoryName 111 | ) { 112 | final var expectedTerms = ""; 113 | 114 | final var aQuery = 115 | new SearchQuery(expectedPage, expectedPerPage, expectedTerms, expectedSort, expectedDirection); 116 | 117 | final var actualResult = useCase.execute(aQuery); 118 | 119 | Assertions.assertEquals(expectedItemsCount, actualResult.items().size()); 120 | Assertions.assertEquals(expectedPage, actualResult.currentPage()); 121 | Assertions.assertEquals(expectedPerPage, actualResult.perPage()); 122 | Assertions.assertEquals(expectedTotal, actualResult.total()); 123 | Assertions.assertEquals(expectedCategoryName, actualResult.items().get(0).name()); 124 | } 125 | 126 | @ParameterizedTest 127 | @CsvSource({ 128 | "0,2,2,7,Amazon Originals;Documentários", 129 | "1,2,2,7,Filmes;Kids", 130 | "2,2,2,7,Netflix Originals;Series", 131 | "3,2,1,7,Sports", 132 | }) 133 | public void givenAValidPage_whenCallsListCategories_shouldReturnCategoriesPaginated( 134 | final int expectedPage, 135 | final int expectedPerPage, 136 | final int expectedItemsCount, 137 | final long expectedTotal, 138 | final String expectedCategoriesName 139 | ) { 140 | final var expectedSort = "name"; 141 | final var expectedDirection = "asc"; 142 | final var expectedTerms = ""; 143 | 144 | final var aQuery = 145 | new SearchQuery(expectedPage, expectedPerPage, expectedTerms, expectedSort, expectedDirection); 146 | 147 | final var actualResult = useCase.execute(aQuery); 148 | 149 | Assertions.assertEquals(expectedItemsCount, actualResult.items().size()); 150 | Assertions.assertEquals(expectedPage, actualResult.currentPage()); 151 | Assertions.assertEquals(expectedPerPage, actualResult.perPage()); 152 | Assertions.assertEquals(expectedTotal, actualResult.total()); 153 | 154 | int index = 0; 155 | for (final String expectedName : expectedCategoriesName.split(";")) { 156 | final String actualName = actualResult.items().get(index).name(); 157 | Assertions.assertEquals(expectedName, actualName); 158 | index++; 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/fullcycle/admin/catalogo/application/category/update/UpdateCategoryUseCaseIT.java: -------------------------------------------------------------------------------- 1 | package com.fullcycle.admin.catalogo.application.category.update; 2 | 3 | import com.fullcycle.admin.catalogo.IntegrationTest; 4 | import com.fullcycle.admin.catalogo.domain.category.Category; 5 | import com.fullcycle.admin.catalogo.domain.category.CategoryGateway; 6 | import com.fullcycle.admin.catalogo.domain.exceptions.DomainException; 7 | import com.fullcycle.admin.catalogo.domain.exceptions.NotFoundException; 8 | import com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryJpaEntity; 9 | import com.fullcycle.admin.catalogo.infrastructure.category.persistence.CategoryRepository; 10 | import org.junit.jupiter.api.Assertions; 11 | import org.junit.jupiter.api.Test; 12 | import org.mockito.Mockito; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.mock.mockito.SpyBean; 15 | 16 | import java.util.Arrays; 17 | 18 | import static org.mockito.ArgumentMatchers.any; 19 | import static org.mockito.Mockito.doThrow; 20 | import static org.mockito.Mockito.times; 21 | 22 | @IntegrationTest 23 | public class UpdateCategoryUseCaseIT { 24 | 25 | @Autowired 26 | private UpdateCategoryUseCase useCase; 27 | 28 | @Autowired 29 | private CategoryRepository categoryRepository; 30 | 31 | @SpyBean 32 | private CategoryGateway categoryGateway; 33 | 34 | @Test 35 | public void givenAValidCommand_whenCallsUpdateCategory_shouldReturnCategoryId() { 36 | final var aCategory = 37 | Category.newCategory("Film", null, true); 38 | 39 | save(aCategory); 40 | 41 | final var expectedName = "Filmes"; 42 | final var expectedDescription = "A categoria mais assistida"; 43 | final var expectedIsActive = true; 44 | final var expectedId = aCategory.getId(); 45 | 46 | final var aCommand = UpdateCategoryCommand.with( 47 | expectedId.getValue(), 48 | expectedName, 49 | expectedDescription, 50 | expectedIsActive 51 | ); 52 | 53 | Assertions.assertEquals(1, categoryRepository.count()); 54 | 55 | final var actualOutput = useCase.execute(aCommand).get(); 56 | 57 | Assertions.assertNotNull(actualOutput); 58 | Assertions.assertNotNull(actualOutput.id()); 59 | 60 | final var actualCategory = 61 | categoryRepository.findById(expectedId.getValue()).get(); 62 | 63 | Assertions.assertEquals(expectedName, actualCategory.getName()); 64 | Assertions.assertEquals(expectedDescription, actualCategory.getDescription()); 65 | Assertions.assertEquals(expectedIsActive, actualCategory.isActive()); 66 | Assertions.assertEquals(aCategory.getCreatedAt(), actualCategory.getCreatedAt()); 67 | Assertions.assertTrue(aCategory.getUpdatedAt().isBefore(actualCategory.getUpdatedAt())); 68 | Assertions.assertNull(actualCategory.getDeletedAt()); 69 | } 70 | 71 | @Test 72 | public void givenAInvalidName_whenCallsUpdateCategory_thenShouldReturnDomainException() { 73 | final var aCategory = 74 | Category.newCategory("Film", null, true); 75 | 76 | save(aCategory); 77 | 78 | final String expectedName = null; 79 | final var expectedDescription = "A categoria mais assistida"; 80 | final var expectedIsActive = true; 81 | final var expectedId = aCategory.getId(); 82 | 83 | final var expectedErrorMessage = "'name' should not be null"; 84 | final var expectedErrorCount = 1; 85 | 86 | final var aCommand = 87 | UpdateCategoryCommand.with(expectedId.getValue(), expectedName, expectedDescription, expectedIsActive); 88 | 89 | final var notification = useCase.execute(aCommand).getLeft(); 90 | 91 | Assertions.assertEquals(expectedErrorCount, notification.getErrors().size()); 92 | Assertions.assertEquals(expectedErrorMessage, notification.firstError().message()); 93 | 94 | Mockito.verify(categoryGateway, times(0)).update(any()); 95 | } 96 | 97 | @Test 98 | public void givenAValidInactivateCommand_whenCallsUpdateCategory_shouldReturnInactiveCategoryId() { 99 | final var aCategory = 100 | Category.newCategory("Film", null, true); 101 | 102 | save(aCategory); 103 | 104 | final var expectedName = "Filmes"; 105 | final var expectedDescription = "A categoria mais assistida"; 106 | final var expectedIsActive = false; 107 | final var expectedId = aCategory.getId(); 108 | 109 | final var aCommand = UpdateCategoryCommand.with( 110 | expectedId.getValue(), 111 | expectedName, 112 | expectedDescription, 113 | expectedIsActive 114 | ); 115 | 116 | Assertions.assertTrue(aCategory.isActive()); 117 | Assertions.assertNull(aCategory.getDeletedAt()); 118 | 119 | final var actualOutput = useCase.execute(aCommand).get(); 120 | 121 | Assertions.assertNotNull(actualOutput); 122 | Assertions.assertNotNull(actualOutput.id()); 123 | 124 | final var actualCategory = 125 | categoryRepository.findById(expectedId.getValue()).get(); 126 | 127 | Assertions.assertEquals(expectedName, actualCategory.getName()); 128 | Assertions.assertEquals(expectedDescription, actualCategory.getDescription()); 129 | Assertions.assertEquals(expectedIsActive, actualCategory.isActive()); 130 | Assertions.assertEquals(aCategory.getCreatedAt(), actualCategory.getCreatedAt()); 131 | Assertions.assertTrue(aCategory.getUpdatedAt().isBefore(actualCategory.getUpdatedAt())); 132 | Assertions.assertNotNull(actualCategory.getDeletedAt()); 133 | } 134 | 135 | @Test 136 | public void givenAValidCommand_whenGatewayThrowsRandomException_shouldReturnAException() { 137 | final var aCategory = 138 | Category.newCategory("Film", null, true); 139 | 140 | save(aCategory); 141 | 142 | final var expectedName = "Filmes"; 143 | final var expectedDescription = "A categoria mais assistida"; 144 | final var expectedIsActive = true; 145 | final var expectedId = aCategory.getId(); 146 | final var expectedErrorCount = 1; 147 | final var expectedErrorMessage = "Gateway error"; 148 | 149 | final var aCommand = UpdateCategoryCommand.with( 150 | expectedId.getValue(), 151 | expectedName, 152 | expectedDescription, 153 | expectedIsActive 154 | ); 155 | 156 | doThrow(new IllegalStateException(expectedErrorMessage)) 157 | .when(categoryGateway).update(any()); 158 | 159 | final var notification = useCase.execute(aCommand).getLeft(); 160 | 161 | Assertions.assertEquals(expectedErrorCount, notification.getErrors().size()); 162 | Assertions.assertEquals(expectedErrorMessage, notification.firstError().message()); 163 | 164 | final var actualCategory = 165 | categoryRepository.findById(expectedId.getValue()).get(); 166 | 167 | Assertions.assertEquals(aCategory.getName(), actualCategory.getName()); 168 | Assertions.assertEquals(aCategory.getDescription(), actualCategory.getDescription()); 169 | Assertions.assertEquals(aCategory.isActive(), actualCategory.isActive()); 170 | Assertions.assertEquals(aCategory.getCreatedAt(), actualCategory.getCreatedAt()); 171 | Assertions.assertEquals(aCategory.getUpdatedAt(), actualCategory.getUpdatedAt()); 172 | Assertions.assertEquals(aCategory.getDeletedAt(), actualCategory.getDeletedAt()); 173 | } 174 | 175 | @Test 176 | public void givenACommandWithInvalidID_whenCallsUpdateCategory_shouldReturnNotFoundException() { 177 | final var expectedName = "Filmes"; 178 | final var expectedDescription = "A categoria mais assistida"; 179 | final var expectedIsActive = false; 180 | final var expectedId = "123"; 181 | final var expectedErrorCount = 1; 182 | final var expectedErrorMessage = "Category with ID 123 was not found"; 183 | 184 | final var aCommand = UpdateCategoryCommand.with( 185 | expectedId, 186 | expectedName, 187 | expectedDescription, 188 | expectedIsActive 189 | ); 190 | 191 | final var actualException = 192 | Assertions.assertThrows(NotFoundException.class, () -> useCase.execute(aCommand)); 193 | 194 | Assertions.assertEquals(expectedErrorMessage, actualException.getMessage()); 195 | } 196 | 197 | private void save(final Category... aCategory) { 198 | categoryRepository.saveAllAndFlush( 199 | Arrays.stream(aCategory) 200 | .map(CategoryJpaEntity::from) 201 | .toList() 202 | ); 203 | } 204 | } --------------------------------------------------------------------------------