diff --git a/codiki-application/pom.xml b/codiki-application/pom.xml index 1e17c96..2f6f156 100644 --- a/codiki-application/pom.xml +++ b/codiki-application/pom.xml @@ -33,5 +33,15 @@ com.auth0 java-jwt + + org.junit.jupiter + junit-jupiter-api + test + + + org.assertj + assertj-core + test + diff --git a/codiki-application/src/main/java/org/codiki/application/configuration/ServiceConfiguration.java b/codiki-application/src/main/java/org/codiki/application/configuration/ServiceConfiguration.java index a3e308e..9a5921a 100644 --- a/codiki-application/src/main/java/org/codiki/application/configuration/ServiceConfiguration.java +++ b/codiki-application/src/main/java/org/codiki/application/configuration/ServiceConfiguration.java @@ -1,5 +1,7 @@ package org.codiki.application.configuration; +import java.time.Clock; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -11,4 +13,9 @@ public class ServiceConfiguration { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + @Bean + public Clock clock() { + return Clock.systemDefaultZone(); + } } diff --git a/codiki-application/src/main/java/org/codiki/application/publication/KeyGenerator.java b/codiki-application/src/main/java/org/codiki/application/publication/KeyGenerator.java new file mode 100644 index 0000000..d92afbc --- /dev/null +++ b/codiki-application/src/main/java/org/codiki/application/publication/KeyGenerator.java @@ -0,0 +1,23 @@ +package org.codiki.application.publication; + +import java.security.SecureRandom; + +import org.springframework.stereotype.Component; + +@Component +public class KeyGenerator { + private static final String ALLOWED_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + private static final int KEY_LENGTH = 10; + + public String generateKey() { + SecureRandom random = new SecureRandom(); + StringBuilder code = new StringBuilder(); + + for (int i = 0; i < KEY_LENGTH; i++) { + int randomIndex = random.nextInt(ALLOWED_CHARACTERS.length()); + code.append(ALLOWED_CHARACTERS.charAt(randomIndex)); + } + + return code.toString(); + } +} diff --git a/codiki-application/src/main/java/org/codiki/application/publication/PublicationCreationRequestValidator.java b/codiki-application/src/main/java/org/codiki/application/publication/PublicationCreationRequestValidator.java new file mode 100644 index 0000000..4b80371 --- /dev/null +++ b/codiki-application/src/main/java/org/codiki/application/publication/PublicationCreationRequestValidator.java @@ -0,0 +1,26 @@ +package org.codiki.application.publication; + +import org.codiki.domain.publication.exception.PublicationCreationException; +import org.codiki.domain.publication.model.PublicationCreationRequest; +import org.springframework.stereotype.Component; + +@Component +public class PublicationCreationRequestValidator { + void isValid(PublicationCreationRequest request) { + if (request.title() == null) { + throw new PublicationCreationException("title cannot be null."); + } + + if (request.text() == null) { + throw new PublicationCreationException("text cannot be null."); + } + + if (request.description() == null) { + throw new PublicationCreationException("description cannot be null."); + } + + if (request.image() == null) { + throw new PublicationCreationException("image cannot be null."); + } + } +} diff --git a/codiki-application/src/main/java/org/codiki/application/publication/PublicationUseCases.java b/codiki-application/src/main/java/org/codiki/application/publication/PublicationUseCases.java new file mode 100644 index 0000000..6f3eb37 --- /dev/null +++ b/codiki-application/src/main/java/org/codiki/application/publication/PublicationUseCases.java @@ -0,0 +1,72 @@ +package org.codiki.application.publication; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.UUID; + +import static org.codiki.domain.publication.model.builder.AuthorBuilder.anAuthor; +import static org.codiki.domain.publication.model.builder.PublicationBuilder.aPublication; +import org.codiki.application.user.UserUseCases; +import org.codiki.domain.category.model.Category; +import org.codiki.domain.category.port.CategoryPort; +import org.codiki.domain.exception.AuthenticationRequiredException; +import org.codiki.domain.publication.exception.PublicationCreationException; +import org.codiki.domain.publication.model.Publication; +import org.codiki.domain.publication.model.PublicationCreationRequest; +import org.codiki.domain.publication.port.PublicationPort; +import org.codiki.domain.user.model.User; +import org.springframework.stereotype.Service; + +@Service +public class PublicationUseCases { + private final CategoryPort categoryPort; + private final KeyGenerator keyGenerator; + private final PublicationPort publicationPort; + private final PublicationCreationRequestValidator publicationCreationRequestValidator; + private final UserUseCases userUseCases; + private final Clock clock; + + public PublicationUseCases( + CategoryPort categoryPort, + KeyGenerator keyGenerator, + PublicationPort publicationPort, + PublicationCreationRequestValidator publicationCreationRequestValidator, + UserUseCases userUseCases, + Clock clock + ) { + this.publicationCreationRequestValidator = publicationCreationRequestValidator; + this.userUseCases = userUseCases; + this.keyGenerator = keyGenerator; + this.clock = clock; + this.categoryPort = categoryPort; + this.publicationPort = publicationPort; + } + + public Publication createPublication(PublicationCreationRequest request) { + publicationCreationRequestValidator.isValid(request); + + User authenticatedUser = userUseCases.getAuthenticatedUser() + .orElseThrow(AuthenticationRequiredException::new); + + Category category = categoryPort.findById(request.categoryId()) + .orElseThrow(() -> new PublicationCreationException( + String.format("No any category exists for id %s", request.categoryId()) + )); + + Publication newPublication = aPublication() + .withId(UUID.randomUUID()) + .withKey(keyGenerator.generateKey()) + .withTitle(request.title()) + .withText(request.text()) + .withDescription(request.description()) + .withImage(request.image()) + .withCreationDate(ZonedDateTime.now(clock)) + .withAuthor(anAuthor().basedOn(authenticatedUser).build()) + .withCategory(category) + .build(); + + publicationPort.save(newPublication); + + return newPublication; + } +} diff --git a/codiki-application/src/test/java/org/codiki/application/publication/KeyGeneratorTest.java b/codiki-application/src/test/java/org/codiki/application/publication/KeyGeneratorTest.java new file mode 100644 index 0000000..2e15770 --- /dev/null +++ b/codiki-application/src/test/java/org/codiki/application/publication/KeyGeneratorTest.java @@ -0,0 +1,31 @@ +package org.codiki.application.publication; + +import java.util.regex.Pattern; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class KeyGeneratorTest { + private KeyGenerator generator; + + @BeforeEach + void setUp() { + generator = new KeyGenerator(); + } + + @Test + public void generateKey_should_generate_random_keys_with_alphanumeric_characters() { + Pattern validationRegex = Pattern.compile("^[0-9A-Z]{10}$"); + + IntStream.range(0, 1000) + .forEach(index -> { + String result = generator.generateKey(); + assertThat(validationRegex.matcher(result).matches()).isTrue(); + }); + } + + + +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/publication/model/Category.java b/codiki-domain/src/main/java/org/codiki/domain/category/model/Category.java similarity index 75% rename from codiki-domain/src/main/java/org/codiki/domain/publication/model/Category.java rename to codiki-domain/src/main/java/org/codiki/domain/category/model/Category.java index e837c4a..47ebf1d 100644 --- a/codiki-domain/src/main/java/org/codiki/domain/publication/model/Category.java +++ b/codiki-domain/src/main/java/org/codiki/domain/category/model/Category.java @@ -1,4 +1,4 @@ -package org.codiki.domain.publication.model; +package org.codiki.domain.category.model; import java.util.List; import java.util.UUID; diff --git a/codiki-domain/src/main/java/org/codiki/domain/category/port/CategoryPort.java b/codiki-domain/src/main/java/org/codiki/domain/category/port/CategoryPort.java new file mode 100644 index 0000000..e22649e --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/category/port/CategoryPort.java @@ -0,0 +1,10 @@ +package org.codiki.domain.category.port; + +import java.util.Optional; +import java.util.UUID; + +import org.codiki.domain.category.model.Category; + +public interface CategoryPort { + Optional findById(final UUID uuid); +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/exception/AuthenticationRequiredException.java b/codiki-domain/src/main/java/org/codiki/domain/exception/AuthenticationRequiredException.java new file mode 100644 index 0000000..a656e9b --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/exception/AuthenticationRequiredException.java @@ -0,0 +1,7 @@ +package org.codiki.domain.exception; + +public class AuthenticationRequiredException extends FunctionnalException { + public AuthenticationRequiredException() { + super("Authentication is required to perform this action."); + } +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/publication/exception/PublicationCreationException.java b/codiki-domain/src/main/java/org/codiki/domain/publication/exception/PublicationCreationException.java new file mode 100644 index 0000000..b1e2c27 --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/publication/exception/PublicationCreationException.java @@ -0,0 +1,9 @@ +package org.codiki.domain.publication.exception; + +import org.codiki.domain.exception.FunctionnalException; + +public class PublicationCreationException extends FunctionnalException { + public PublicationCreationException(String reason) { + super(String.format("Impossible to create a publication because : %s", reason)); + } +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/publication/model/Author.java b/codiki-domain/src/main/java/org/codiki/domain/publication/model/Author.java index 7b1fbef..b4d7c2a 100644 --- a/codiki-domain/src/main/java/org/codiki/domain/publication/model/Author.java +++ b/codiki-domain/src/main/java/org/codiki/domain/publication/model/Author.java @@ -2,9 +2,12 @@ package org.codiki.domain.publication.model; import java.util.UUID; +import org.codiki.domain.user.model.User; + public record Author( UUID id, String name, String image ) { + } diff --git a/codiki-domain/src/main/java/org/codiki/domain/publication/model/Publication.java b/codiki-domain/src/main/java/org/codiki/domain/publication/model/Publication.java index 73f1e14..57bf2e8 100644 --- a/codiki-domain/src/main/java/org/codiki/domain/publication/model/Publication.java +++ b/codiki-domain/src/main/java/org/codiki/domain/publication/model/Publication.java @@ -3,6 +3,8 @@ package org.codiki.domain.publication.model; import java.time.ZonedDateTime; import java.util.UUID; +import org.codiki.domain.category.model.Category; + public record Publication( UUID id, String key, diff --git a/codiki-domain/src/main/java/org/codiki/domain/publication/model/PublicationCreationRequest.java b/codiki-domain/src/main/java/org/codiki/domain/publication/model/PublicationCreationRequest.java new file mode 100644 index 0000000..3963543 --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/publication/model/PublicationCreationRequest.java @@ -0,0 +1,11 @@ +package org.codiki.domain.publication.model; + +import java.util.UUID; + +public record PublicationCreationRequest( + String title, + String text, + String description, + String image, + UUID categoryId +) {} diff --git a/codiki-domain/src/main/java/org/codiki/domain/publication/model/builder/AuthorBuilder.java b/codiki-domain/src/main/java/org/codiki/domain/publication/model/builder/AuthorBuilder.java new file mode 100644 index 0000000..e373168 --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/publication/model/builder/AuthorBuilder.java @@ -0,0 +1,43 @@ +package org.codiki.domain.publication.model.builder; + +import java.util.UUID; + +import org.codiki.domain.publication.model.Author; +import org.codiki.domain.user.model.User; + +public class AuthorBuilder { + private UUID id; + private String name; + private String image; + + private AuthorBuilder() {} + + public static AuthorBuilder anAuthor() { + return new AuthorBuilder(); + } + + public AuthorBuilder basedOn(User user) { + return new AuthorBuilder() + .withId(user.id()) +// .withName(user.name()) +// .withImage(user.image()) + ; + } + + public AuthorBuilder withId(UUID id) { + this.id = id; + return this; + } + public AuthorBuilder withName(String name) { + this.name = name; + return this; + } + public AuthorBuilder withImage(String image) { + this.image = image; + return this; + } + + public Author build() { + return new Author(id, name, image); + } +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/publication/model/builder/PublicationBuilder.java b/codiki-domain/src/main/java/org/codiki/domain/publication/model/builder/PublicationBuilder.java new file mode 100644 index 0000000..3782bdc --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/publication/model/builder/PublicationBuilder.java @@ -0,0 +1,85 @@ +package org.codiki.domain.publication.model.builder; + +import java.time.ZonedDateTime; +import java.util.UUID; + +import org.codiki.domain.publication.model.Author; +import org.codiki.domain.category.model.Category; +import org.codiki.domain.publication.model.Publication; + +public class PublicationBuilder { + private UUID id; + private String key; + private String title; + private String text; + private String description; + private String image; + private ZonedDateTime creationDate; + private Author author; + private Category category; + + private PublicationBuilder() {} + + public static PublicationBuilder aPublication() { + return new PublicationBuilder(); + } + + public PublicationBuilder withId(UUID id) { + this.id = id; + return this; + } + + public PublicationBuilder withKey(String key) { + this.key = key; + return this; + } + + public PublicationBuilder withTitle(String title) { + this.title = title; + return this; + } + + public PublicationBuilder withText(String text) { + this.text = text; + return this; + } + + public PublicationBuilder withDescription(String description) { + this.description = description; + return this; + } + + public PublicationBuilder withImage(String image) { + this.image = image; + return this; + } + + public PublicationBuilder withCreationDate(ZonedDateTime creationDate) { + this.creationDate = creationDate; + return this; + } + + public PublicationBuilder withAuthor(Author author) { + this.author = author; + return this; + } + + public PublicationBuilder withCategory(Category category) { + this.category = category; + return this; + } + + public Publication build() { + return new Publication( + id, + key, + title, + text, + description, + image, + creationDate, + author, + category + ); + } +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/publication/port/PublicationPort.java b/codiki-domain/src/main/java/org/codiki/domain/publication/port/PublicationPort.java new file mode 100644 index 0000000..a060c32 --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/publication/port/PublicationPort.java @@ -0,0 +1,7 @@ +package org.codiki.domain.publication.port; + +import org.codiki.domain.publication.model.Publication; + +public interface PublicationPort { + void save(Publication publication); +}