Add implementation of publication creation use case.

This commit is contained in:
Florian THIERRY
2024-03-11 13:57:49 +01:00
parent bc62939740
commit c19bd5407f
16 changed files with 347 additions and 1 deletions

View File

@@ -33,5 +33,15 @@
<groupId>com.auth0</groupId> <groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId> <artifactId>java-jwt</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -1,5 +1,7 @@
package org.codiki.application.configuration; package org.codiki.application.configuration;
import java.time.Clock;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@@ -11,4 +13,9 @@ public class ServiceConfiguration {
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
} }
@Bean
public Clock clock() {
return Clock.systemDefaultZone();
}
} }

View File

@@ -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();
}
}

View File

@@ -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.");
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
});
}
}

View File

@@ -1,4 +1,4 @@
package org.codiki.domain.publication.model; package org.codiki.domain.category.model;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;

View File

@@ -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<Category> findById(final UUID uuid);
}

View File

@@ -0,0 +1,7 @@
package org.codiki.domain.exception;
public class AuthenticationRequiredException extends FunctionnalException {
public AuthenticationRequiredException() {
super("Authentication is required to perform this action.");
}
}

View File

@@ -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));
}
}

View File

@@ -2,9 +2,12 @@ package org.codiki.domain.publication.model;
import java.util.UUID; import java.util.UUID;
import org.codiki.domain.user.model.User;
public record Author( public record Author(
UUID id, UUID id,
String name, String name,
String image String image
) { ) {
} }

View File

@@ -3,6 +3,8 @@ package org.codiki.domain.publication.model;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.UUID; import java.util.UUID;
import org.codiki.domain.category.model.Category;
public record Publication( public record Publication(
UUID id, UUID id,
String key, String key,

View File

@@ -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
) {}

View File

@@ -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);
}
}

View File

@@ -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
);
}
}

View File

@@ -0,0 +1,7 @@
package org.codiki.domain.publication.port;
import org.codiki.domain.publication.model.Publication;
public interface PublicationPort {
void save(Publication publication);
}