diff --git a/backend/src/test/java/dev/flima/application/educations/usecases/CreateEducationUseCaseTest.java b/backend/src/test/java/dev/flima/application/educations/usecases/CreateEducationUseCaseTest.java new file mode 100644 index 0000000..55335d2 --- /dev/null +++ b/backend/src/test/java/dev/flima/application/educations/usecases/CreateEducationUseCaseTest.java @@ -0,0 +1,73 @@ +package dev.flima.application.educations.usecases; + +import dev.flima.application.educations.dtos.request.EducationDTORequest; +import dev.flima.application.educations.dtos.response.CreateEducationDTOResponse; +import dev.flima.domain.educations.Education; +import dev.flima.domain.educations.EducationRepository; +import dev.flima.domain.educations.TypeEducation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class CreateEducationUseCaseTest { + + @Mock + private EducationRepository educationRepository; + + private CreateEducationUseCase createEducationUseCase; + + @BeforeEach + void setUp() { + createEducationUseCase = new CreateEducationUseCase(educationRepository); + } + + @Test + @DisplayName("Should create an education entry successfully") + void shouldCreateEducationSuccessfully() { + // Arrange + EducationDTORequest request = new EducationDTORequest( + TypeEducation.DEGREE, + "Bachelor", + "Computer Science", + "Tech University", + "2018-2022", + "Software Engineering", + List.of("Java", "Spring"), + List.of("Clean Architecture") + ); + + // Act + CreateEducationDTOResponse response = createEducationUseCase.execute(request); + + // Assert + assertNotNull(response); + assertNotNull(response.id()); + assertEquals("Computer Science", response.title()); + + // Verify repository interaction + ArgumentCaptor educationCaptor = ArgumentCaptor.forClass(Education.class); + verify(educationRepository, times(1)).save(educationCaptor.capture()); + + Education savedEducation = educationCaptor.getValue(); + assertEquals(TypeEducation.DEGREE, savedEducation.getTypeEducation()); + assertEquals("Bachelor", savedEducation.getDegree()); + assertEquals("Computer Science", savedEducation.getTitle()); + assertEquals("Tech University", savedEducation.getInstitution()); + assertEquals("2018-2022", savedEducation.getPeriod()); + assertEquals("Software Engineering", savedEducation.getSpecialization()); + assertEquals(List.of("Java", "Spring"), savedEducation.getSkills()); + assertEquals(List.of("Clean Architecture"), savedEducation.getArchitectures()); + } +} diff --git a/backend/src/test/java/dev/flima/application/educations/usecases/DeleteEducationUseCaseTest.java b/backend/src/test/java/dev/flima/application/educations/usecases/DeleteEducationUseCaseTest.java new file mode 100644 index 0000000..685b473 --- /dev/null +++ b/backend/src/test/java/dev/flima/application/educations/usecases/DeleteEducationUseCaseTest.java @@ -0,0 +1,59 @@ +package dev.flima.application.educations.usecases; + +import dev.flima.domain.educations.Education; +import dev.flima.domain.educations.EducationRepository; +import dev.flima.domain.educations.TypeEducation; +import dev.flima.domain.exceptions.EntityNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class DeleteEducationUseCaseTest { + + @Mock + private EducationRepository educationRepository; + + private DeleteEducationUseCase deleteEducationUseCase; + + @BeforeEach + void setUp() { + deleteEducationUseCase = new DeleteEducationUseCase(educationRepository); + } + + @Test + @DisplayName("Should delete education successfully") + void shouldDeleteSuccessfully() { + // Arrange + UUID id = UUID.randomUUID(); + Education education = new Education(TypeEducation.DEGREE, "D", "T", "I", "P", "S", List.of(), List.of()); + when(educationRepository.getById(id)).thenReturn(Optional.of(education)); + + // Act + deleteEducationUseCase.execute(id); + + // Assert + verify(educationRepository, times(1)).remove(education); + } + + @Test + @DisplayName("Should throw EntityNotFoundException when deleting non-existent education") + void shouldThrowExceptionWhenNotFound() { + // Arrange + UUID id = UUID.randomUUID(); + when(educationRepository.getById(id)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(EntityNotFoundException.class, () -> deleteEducationUseCase.execute(id)); + } +} diff --git a/backend/src/test/java/dev/flima/application/educations/usecases/UpdateEducationUseCaseTest.java b/backend/src/test/java/dev/flima/application/educations/usecases/UpdateEducationUseCaseTest.java new file mode 100644 index 0000000..365bb12 --- /dev/null +++ b/backend/src/test/java/dev/flima/application/educations/usecases/UpdateEducationUseCaseTest.java @@ -0,0 +1,92 @@ +package dev.flima.application.educations.usecases; + +import dev.flima.application.educations.dtos.request.EducationDTORequest; +import dev.flima.application.educations.dtos.response.EducationDTOResponse; +import dev.flima.domain.educations.Education; +import dev.flima.domain.educations.EducationRepository; +import dev.flima.domain.educations.TypeEducation; +import dev.flima.domain.exceptions.EntityNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UpdateEducationUseCaseTest { + + @Mock + private EducationRepository educationRepository; + + private UpdateEducationUseCase updateEducationUseCase; + + @BeforeEach + void setUp() { + updateEducationUseCase = new UpdateEducationUseCase(educationRepository); + } + + @Test + @DisplayName("Should update education successfully") + void shouldUpdateSuccessfully() { + // Arrange + UUID id = UUID.randomUUID(); + Education existingEducation = new Education( + TypeEducation.DEGREE, + "Old Degree", + "Old Title", + "Old Inst", + "2020", + "Old Spec", + List.of(), + List.of() + ); + // Set ID manually since it's usually generated by DB or set during creation + // But for mock existing we need to make sure it looks real + + when(educationRepository.getById(id)).thenReturn(Optional.of(existingEducation)); + + EducationDTORequest updateRequest = new EducationDTORequest( + TypeEducation.DEGREE, + "New Degree", + "New Title", + "New Inst", + "2021", + "New Spec", + List.of("Skill"), + List.of("Arch") + ); + + // Act + EducationDTOResponse response = updateEducationUseCase.execute(id, updateRequest); + + // Assert + assertNotNull(response); + assertEquals("New Title", response.title()); + assertEquals("New Degree", response.degree()); + + verify(educationRepository, times(1)).modify(any(Education.class)); + } + + @Test + @DisplayName("Should throw EntityNotFoundException when education does not exist") + void shouldThrowExceptionWhenNotFound() { + // Arrange + UUID id = UUID.randomUUID(); + when(educationRepository.getById(id)).thenReturn(Optional.empty()); + + EducationDTORequest request = new EducationDTORequest( + TypeEducation.DEGREE, "D", "T", "I", "P", "S", List.of(), List.of() + ); + + // Act & Assert + assertThrows(EntityNotFoundException.class, () -> updateEducationUseCase.execute(id, request)); + } +} diff --git a/backend/src/test/java/dev/flima/application/users/usecases/CreateUserUseCaseTest.java b/backend/src/test/java/dev/flima/application/users/usecases/CreateUserUseCaseTest.java new file mode 100644 index 0000000..a74ed17 --- /dev/null +++ b/backend/src/test/java/dev/flima/application/users/usecases/CreateUserUseCaseTest.java @@ -0,0 +1,78 @@ +package dev.flima.application.users.usecases; + +import dev.flima.application.users.dtos.request.UserDTORequest; +import dev.flima.application.users.dtos.response.UserDTOResponse; +import dev.flima.domain.security.PasswordHasher; +import dev.flima.domain.users.Password; +import dev.flima.domain.users.Role; +import dev.flima.domain.users.User; +import dev.flima.domain.users.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CreateUserUseCaseTest { + + @Mock + private UserRepository userRepository; + + @Mock + private PasswordHasher passwordHasher; + + private CreateUserUseCase createUserUseCase; + + @BeforeEach + void setUp() { + createUserUseCase = new CreateUserUseCase(userRepository, passwordHasher); + } + + @Test + @DisplayName("Should create a user successfully") + void shouldCreateUserSuccessfully() { + // Arrange + String passwordRaw = "password123"; + String passwordHashed = "hashed_password"; + UserDTORequest request = new UserDTORequest( + "johndoe", + "John", + "Doe", + "john@example.com", + Role.OWNER, + new Password(passwordRaw) + ); + + when(passwordHasher.hash(any(Password.class))).thenReturn(passwordHashed); + + // Act + UserDTOResponse response = createUserUseCase.execute(request); + + // Assert + assertNotNull(response); + assertEquals("johndoe", response.username()); + assertEquals(Role.OWNER, response.role()); + + // Verify repository interaction + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + verify(userRepository, times(1)).save(userCaptor.capture()); + + User savedUser = userCaptor.getValue(); + assertEquals("johndoe", savedUser.getUsername()); + assertEquals("John", savedUser.getName()); + assertEquals("Doe", savedUser.getLastName()); + assertEquals("john@example.com", savedUser.getEmail()); + assertEquals(Role.OWNER, savedUser.getRole()); + assertEquals(passwordHashed, savedUser.getPassword().password()); + + verify(passwordHasher, times(1)).hash(any(Password.class)); + } +} diff --git a/backend/src/test/java/dev/flima/infrastructure/exceptions/ExceptionHandlerTest.java b/backend/src/test/java/dev/flima/infrastructure/exceptions/ExceptionHandlerTest.java new file mode 100644 index 0000000..6b959b2 --- /dev/null +++ b/backend/src/test/java/dev/flima/infrastructure/exceptions/ExceptionHandlerTest.java @@ -0,0 +1,31 @@ +package dev.flima.infrastructure.exceptions; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; + +@QuarkusTest +class ExceptionHandlerTest { + + @Test + @DisplayName("Should return consistent ErrorResponse for DomainException (404)") + @TestSecurity(user = "admin", roles = "OWNER") + void shouldReturnConsistentErrorResponse() { + given() + .when() + .get("/educations/" + UUID.randomUUID()) + .then() + .statusCode(404) + .body("message", is("Education record not found.")) + .body("status", is(404)) + .body("timestamp", notNullValue()); + } +} diff --git a/backend/src/test/java/dev/flima/infrastructure/messaging/ContactMessagingAdapterTest.java b/backend/src/test/java/dev/flima/infrastructure/messaging/ContactMessagingAdapterTest.java new file mode 100644 index 0000000..a464536 --- /dev/null +++ b/backend/src/test/java/dev/flima/infrastructure/messaging/ContactMessagingAdapterTest.java @@ -0,0 +1,56 @@ +package dev.flima.infrastructure.messaging; + +import dev.flima.domain.messages.Message; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.kafka.KafkaCompanionResource; +import io.quarkus.test.kafka.InjectKafkaCompanion; +import io.smallrye.reactive.messaging.kafka.companion.KafkaCompanion; +import jakarta.inject.Inject; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.UUID; + +@QuarkusTest +@QuarkusTestResource(KafkaCompanionResource.class) +class ContactMessagingAdapterTest { + + @Inject + ContactMessagingAdapter contactMessagingAdapter; + + @InjectKafkaCompanion + KafkaCompanion companion; + + @Test + @DisplayName("Should produce a message to Kafka topic when sendMessage is called") + void shouldProduceMessageToKafka() { + String uniqueUser = "tester-" + UUID.randomUUID(); + // Arrange + Message message = new Message( + uniqueUser, + "tester@example.com", + "Subject", + "Body" + ); + + // Act + contactMessagingAdapter.sendMessage(message); + + // Assert + Awaitility.await() + .atMost(Duration.ofSeconds(15)) + .untilAsserted(() -> { + var records = companion.consumeStrings() + .fromTopics("contact-messages", 1) + .awaitCompletion(Duration.ofSeconds(10)) + .getRecords(); + + boolean found = records.stream() + .anyMatch(record -> record.value().contains("\"username\":\"" + uniqueUser + "\"")); + org.junit.jupiter.api.Assertions.assertTrue(found, "Message not found in Kafka topic: " + uniqueUser); + }); + } +} diff --git a/backend/src/test/java/dev/flima/infrastructure/messaging/KafkaResilienceTest.java b/backend/src/test/java/dev/flima/infrastructure/messaging/KafkaResilienceTest.java new file mode 100644 index 0000000..9ea9822 --- /dev/null +++ b/backend/src/test/java/dev/flima/infrastructure/messaging/KafkaResilienceTest.java @@ -0,0 +1,43 @@ +package dev.flima.infrastructure.messaging; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.kafka.InjectKafkaCompanion; +import io.quarkus.test.kafka.KafkaCompanionResource; +import io.smallrye.reactive.messaging.kafka.companion.KafkaCompanion; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +@QuarkusTest +@QuarkusTestResource(KafkaCompanionResource.class) +class KafkaResilienceTest { + + @InjectKafkaCompanion + KafkaCompanion kafkaCompanion; + + @Test + @DisplayName("Should send message to DLQ when JSON is invalid") + void shouldSendToDlqOnInvalidJson() { + // 1. Produce a "poison pill" (invalid JSON) + kafkaCompanion.produceStrings() + .fromRecords(new org.apache.kafka.clients.producer.ProducerRecord<>("contact-messages", "INVALID_JSON_PAYLOAD")) + .awaitCompletion(); + + // 2. Verify it landed in the DLQ topic + Awaitility.await() + .atMost(Duration.ofSeconds(20)) + .untilAsserted(() -> { + var dlqConsumer = kafkaCompanion.consumeStrings() + .fromTopics("contact-messages-dlq", 1) + .awaitCompletion(Duration.ofSeconds(10)); + + assertTrue(dlqConsumer.count() >= 1, "Poison pill should be in DLQ"); + assertTrue(dlqConsumer.getFirstRecord().value().contains("INVALID_JSON_PAYLOAD")); + }); + } +} diff --git a/backend/src/test/java/dev/flima/presentation/rest/auth/AuthFailureTest.java b/backend/src/test/java/dev/flima/presentation/rest/auth/AuthFailureTest.java new file mode 100644 index 0000000..a57eb90 --- /dev/null +++ b/backend/src/test/java/dev/flima/presentation/rest/auth/AuthFailureTest.java @@ -0,0 +1,33 @@ +package dev.flima.presentation.rest.auth; + +import dev.flima.application.auth.dtos.request.LoginDTORequest; +import dev.flima.domain.users.Password; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +class AuthFailureTest { + + @Test + @DisplayName("Should return 404 when login fails (wrong username or password)") + void shouldReturn404ForFailedLogin() { + LoginDTORequest request = new LoginDTORequest( + "non_existent_user", + new Password("wrong_password") + ); + + given() + .contentType(ContentType.JSON) + .body(request) + .when() + .post("/auth") + .then() + .statusCode(404) + .body("message", is("Invalid credentials. Please try again.")); + } +} diff --git a/backend/src/test/java/dev/flima/presentation/rest/auth/AuthResourceTest.java b/backend/src/test/java/dev/flima/presentation/rest/auth/AuthResourceTest.java new file mode 100644 index 0000000..0e30a62 --- /dev/null +++ b/backend/src/test/java/dev/flima/presentation/rest/auth/AuthResourceTest.java @@ -0,0 +1,72 @@ +package dev.flima.presentation.rest.auth; + +import dev.flima.application.auth.dtos.request.LoginDTORequest; +import dev.flima.application.users.dtos.request.UserDTORequest; +import dev.flima.domain.users.Password; +import dev.flima.domain.users.Role; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; + +@QuarkusTest +class AuthResourceTest { + + @Test + @DisplayName("Should login successfully and return token") + void shouldLoginSuccessfully() { + // 1. Create a user + UserDTORequest userRequest = new UserDTORequest( + "auth_tester", + "Auth", + "Tester", + "auth@example.com", + Role.OWNER, + new Password("secret123") + ); + + given() + .contentType(ContentType.JSON) + .body(userRequest) + .post("/__users") + .then() + .statusCode(201); + + // 2. Try to login + LoginDTORequest loginRequest = new LoginDTORequest( + "auth_tester", + new Password("secret123") + ); + + given() + .contentType(ContentType.JSON) + .body(loginRequest) + .when() + .post("/auth") + .then() + .statusCode(202) + .body("username", is("auth_tester")) + .body("token", notNullValue()); + } + + @Test + @DisplayName("Should return 404 for invalid credentials") + void shouldReturnErrorForInvalidLogin() { + LoginDTORequest loginRequest = new LoginDTORequest( + "nonexistent", + new Password("wrongpassword") + ); + + given() + .contentType(ContentType.JSON) + .body(loginRequest) + .when() + .post("/auth") + .then() + .statusCode(404); + } +} diff --git a/backend/src/test/java/dev/flima/presentation/rest/educations/EducationResourceCRUDTest.java b/backend/src/test/java/dev/flima/presentation/rest/educations/EducationResourceCRUDTest.java new file mode 100644 index 0000000..827d3c4 --- /dev/null +++ b/backend/src/test/java/dev/flima/presentation/rest/educations/EducationResourceCRUDTest.java @@ -0,0 +1,87 @@ +package dev.flima.presentation.rest.educations; + +import dev.flima.application.educations.dtos.request.EducationDTORequest; +import dev.flima.application.educations.dtos.response.CreateEducationDTOResponse; +import dev.flima.domain.educations.TypeEducation; +import dev.flima.domain.users.Role; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +class EducationResourceCRUDTest { + + @Test + @DisplayName("Should update and delete education entry as OWNER") + @TestSecurity(user = "admin", roles = Role.Labels.OWNER) + void shouldUpdateAndDeleteEducation() { + // 1. Create + EducationDTORequest request = new EducationDTORequest( + TypeEducation.CERTIFICATION, + "Cert", + "AWS Solutions Architect", + "Amazon", + "2023", + "Cloud", + List.of("AWS"), + List.of("Cloud Native") + ); + + CreateEducationDTOResponse createResponse = given() + .contentType(ContentType.JSON) + .body(request) + .post("/educations") + .then() + .statusCode(201) + .extract().as(CreateEducationDTOResponse.class); + + UUID id = createResponse.id(); + + // 2. Update + EducationDTORequest updateRequest = new EducationDTORequest( + TypeEducation.CERTIFICATION, + "Cert", + "AWS Solutions Architect Professional", + "Amazon", + "2023", + "Cloud", + List.of("AWS", "Serverless"), + List.of("Cloud Native") + ); + + given() + .contentType(ContentType.JSON) + .body(updateRequest) + .put("/educations/" + id) + .then() + .statusCode(200) + .body("title", is("AWS Solutions Architect Professional")); + + // 3. Delete + given() + .when() + .delete("/educations/" + id) + .then() + .statusCode(204); + } + + @Test + @DisplayName("Should return 401 for update without auth") + void shouldReturnUnauthorizedForUpdate() { + given() + .contentType(ContentType.JSON) + .body("{}") + .when() + .put("/educations/" + UUID.randomUUID()) + .then() + .statusCode(401); + } +} diff --git a/backend/src/test/java/dev/flima/presentation/rest/experiences/ExperienceResourceTest.java b/backend/src/test/java/dev/flima/presentation/rest/experiences/ExperienceResourceTest.java new file mode 100644 index 0000000..ba55883 --- /dev/null +++ b/backend/src/test/java/dev/flima/presentation/rest/experiences/ExperienceResourceTest.java @@ -0,0 +1,21 @@ +package dev.flima.presentation.rest.experiences; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; + +@QuarkusTest +class ExperienceResourceTest { + + @Test + @DisplayName("Should return 200 for public experience list") + void shouldReturnPublicExperiences() { + given() + .when() + .get("/experiences") + .then() + .statusCode(200); + } +} diff --git a/backend/src/test/java/dev/flima/presentation/rest/messages/MessageResourceTest.java b/backend/src/test/java/dev/flima/presentation/rest/messages/MessageResourceTest.java new file mode 100644 index 0000000..7b436bf --- /dev/null +++ b/backend/src/test/java/dev/flima/presentation/rest/messages/MessageResourceTest.java @@ -0,0 +1,67 @@ +package dev.flima.presentation.rest.messages; + +import dev.flima.application.messages.dtos.request.MessageDTORequest; +import dev.flima.domain.users.Role; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +class MessageResourceTest { + + @Test + @DisplayName("Should create a message successfully (PermitAll)") + void shouldCreateMessage() { + MessageDTORequest request = new MessageDTORequest( + "visitor", + "visitor@example.com", + "Hello", + "This is a test message" + ); + + given() + .contentType(ContentType.JSON) + .body(request) + .when() + .post("/messages") + .then() + .statusCode(201); + } + + @Test + @DisplayName("Should return 401 when getting all messages without authentication") + void shouldReturnUnauthorizedForGetAll() { + given() + .when() + .get("/messages") + .then() + .statusCode(401); + } + + @Test + @DisplayName("Should return 403 when getting all messages with wrong role") + @TestSecurity(user = "user", roles = "USER") + void shouldReturnForbiddenForGetAll() { + given() + .when() + .get("/messages") + .then() + .statusCode(403); + } + + @Test + @DisplayName("Should get all messages when authenticated as OWNER") + @TestSecurity(user = "admin", roles = Role.Labels.OWNER) + void shouldGetAllMessagesAsOwner() { + given() + .when() + .get("/messages") + .then() + .statusCode(200); + } +} diff --git a/backend/src/test/java/dev/flima/presentation/rest/messages/MessageValidationTest.java b/backend/src/test/java/dev/flima/presentation/rest/messages/MessageValidationTest.java new file mode 100644 index 0000000..ef544cc --- /dev/null +++ b/backend/src/test/java/dev/flima/presentation/rest/messages/MessageValidationTest.java @@ -0,0 +1,39 @@ +package dev.flima.presentation.rest.messages; + +import dev.flima.application.messages.dtos.request.MessageDTORequest; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasItem; + +@QuarkusTest +class MessageValidationTest { + + @Test + @DisplayName("Should return 400 when sending message with empty fields") + void shouldReturnErrorForEmptyMessage() { + MessageDTORequest request = new MessageDTORequest( + "", + "invalid", + "", + "" + ); + + given() + .contentType(ContentType.JSON) + .body(request) + .when() + .post("/messages") + .then() + .statusCode(400) + .body("message", is("Validation failed")) + .body("details", hasItem(is("create.messageDTO.username: Username is required."))) + .body("details", hasItem(is("create.messageDTO.email: Please provide a valid email address."))) + .body("details", hasItem(is("create.messageDTO.subject: Subject is required to send a message."))) + .body("details", hasItem(is("create.messageDTO.message: Message body cannot be empty."))); + } +} diff --git a/backend/src/test/java/dev/flima/presentation/rest/projects/ProjectResourceTest.java b/backend/src/test/java/dev/flima/presentation/rest/projects/ProjectResourceTest.java new file mode 100644 index 0000000..700d509 --- /dev/null +++ b/backend/src/test/java/dev/flima/presentation/rest/projects/ProjectResourceTest.java @@ -0,0 +1,34 @@ +package dev.flima.presentation.rest.projects; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; + +@QuarkusTest +class ProjectResourceTest { + + @Test + @DisplayName("Should return 200 for public project list") + void shouldReturnPublicProjects() { + given() + .when() + .get("/projects") + .then() + .statusCode(200); + } + + @Test + @DisplayName("Should return 401 for protected creation without auth") + void shouldReturnUnauthorizedForCreate() { + given() + .contentType(ContentType.JSON) + .body("{}") + .when() + .post("/projects") + .then() + .statusCode(401); + } +} diff --git a/backend/src/test/java/dev/flima/presentation/rest/security/JwtSecurityTest.java b/backend/src/test/java/dev/flima/presentation/rest/security/JwtSecurityTest.java new file mode 100644 index 0000000..d38ee80 --- /dev/null +++ b/backend/src/test/java/dev/flima/presentation/rest/security/JwtSecurityTest.java @@ -0,0 +1,63 @@ +package dev.flima.presentation.rest.security; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import io.smallrye.jwt.build.Jwt; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Set; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +class JwtSecurityTest { + + @Test + @DisplayName("Should reject request with malformed token") + void shouldRejectMalformedToken() { + given() + .header("Authorization", "Bearer this_is_not_a_valid_jwt") + .when() + .get("/messages") + .then() + .statusCode(401); + } + + @Test + @DisplayName("Should reject expired token") + void shouldRejectExpiredToken() { + // Token expired 1 hour ago + String expiredToken = Jwt.issuer("https://flima.dev") + .upn("admin") + .groups(Set.of("OWNER")) + .expiresAt(Instant.now().getEpochSecond() - 3600) + .sign(); + + given() + .header("Authorization", "Bearer " + expiredToken) + .when() + .get("/messages") + .then() + .statusCode(401); + } + + @Test + @DisplayName("Should reject token with wrong issuer") + void shouldRejectWrongIssuer() { + String wrongIssuerToken = Jwt.issuer("https://hacker.com") + .upn("admin") + .groups(Set.of("OWNER")) + .expiresAt(Instant.now().getEpochSecond() + 3600) + .sign(); + + given() + .header("Authorization", "Bearer " + wrongIssuerToken) + .when() + .get("/messages") + .then() + .statusCode(401); + } +} diff --git a/backend/src/test/java/dev/flima/presentation/rest/stacks/StackResourceTest.java b/backend/src/test/java/dev/flima/presentation/rest/stacks/StackResourceTest.java new file mode 100644 index 0000000..4557bab --- /dev/null +++ b/backend/src/test/java/dev/flima/presentation/rest/stacks/StackResourceTest.java @@ -0,0 +1,21 @@ +package dev.flima.presentation.rest.stacks; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; + +@QuarkusTest +class StackResourceTest { + + @Test + @DisplayName("Should return 200 for public stack list") + void shouldReturnPublicStacks() { + given() + .when() + .get("/stacks") + .then() + .statusCode(200); + } +} diff --git a/backend/src/test/java/dev/flima/presentation/rest/stats/StatResourceTest.java b/backend/src/test/java/dev/flima/presentation/rest/stats/StatResourceTest.java new file mode 100644 index 0000000..8b3afbc --- /dev/null +++ b/backend/src/test/java/dev/flima/presentation/rest/stats/StatResourceTest.java @@ -0,0 +1,50 @@ +package dev.flima.presentation.rest.stats; + +import dev.flima.application.stats.dtos.request.StatDTORequest; +import dev.flima.domain.users.Role; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; + +@QuarkusTest +class StatResourceTest { + + @Test + @DisplayName("Should return 200 for public stats list") + void shouldReturnPublicStats() { + given() + .when() + .get("/stats") + .then() + .statusCode(200); + } + + @Test + @DisplayName("Should return 401 for update without auth") + void shouldReturnUnauthorizedForUpdate() { + given() + .contentType(ContentType.JSON) + .body("{}") + .when() + .put("/stats/id") + .then() + .statusCode(401); + } + + @Test + @DisplayName("Should return 403 for update as common user") + @TestSecurity(user = "user", roles = "USER") + void shouldReturnForbiddenForUpdate() { + given() + .contentType(ContentType.JSON) + .body("{}") + .when() + .put("/stats/id") + .then() + .statusCode(403); + } +} diff --git a/backend/src/test/java/dev/flima/presentation/rest/users/UserDuplicateTest.java b/backend/src/test/java/dev/flima/presentation/rest/users/UserDuplicateTest.java new file mode 100644 index 0000000..50bc2b8 --- /dev/null +++ b/backend/src/test/java/dev/flima/presentation/rest/users/UserDuplicateTest.java @@ -0,0 +1,55 @@ +package dev.flima.presentation.rest.users; + +import dev.flima.application.users.dtos.request.UserDTORequest; +import dev.flima.domain.users.Password; +import dev.flima.domain.users.Role; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; + +@QuarkusTest +class UserDuplicateTest { + + @Test + @DisplayName("Should return error when creating user with duplicate username") + void shouldReturnErrorForDuplicateUsername() { + UserDTORequest request = new UserDTORequest( + "duplicate_user", + "Name", + "Last", + "dup1@example.com", + Role.OWNER, + new Password("password123") + ); + + // First creation + given() + .contentType(ContentType.JSON) + .body(request) + .when() + .post("/__users") + .then() + .statusCode(201); + + // Second creation with same username, different email + UserDTORequest request2 = new UserDTORequest( + "duplicate_user", + "Name", + "Last", + "dup2@example.com", + Role.OWNER, + new Password("password123") + ); + + given() + .contentType(ContentType.JSON) + .body(request2) + .when() + .post("/__users") + .then() + .statusCode(409); // Expecting Conflict or similar + } +} diff --git a/backend/src/test/java/dev/flima/presentation/rest/users/UserResourceTest.java b/backend/src/test/java/dev/flima/presentation/rest/users/UserResourceTest.java new file mode 100644 index 0000000..d141804 --- /dev/null +++ b/backend/src/test/java/dev/flima/presentation/rest/users/UserResourceTest.java @@ -0,0 +1,77 @@ +package dev.flima.presentation.rest.users; + +import dev.flima.application.users.dtos.request.UserDTORequest; +import dev.flima.domain.users.Password; +import dev.flima.domain.users.Role; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +class UserResourceTest { + + @Test + @DisplayName("Should create a user successfully") + void shouldCreateUser() { + UserDTORequest request = new UserDTORequest( + "tester", + "Test", + "User", + "tester@example.com", + Role.OWNER, + new Password("password123") + ); + + given() + .contentType(ContentType.JSON) + .body(request) + .when() + .post("/__users") + .then() + .statusCode(201); + } + + @Test + @DisplayName("Should get a user when authenticated") + @TestSecurity(user = "tester", roles = "OWNER") + void shouldGetUser() { + // First create a user to ensure it exists in the DB + UserDTORequest request = new UserDTORequest( + "tester2", + "Test", + "User", + "tester2@example.com", + Role.OWNER, + new Password("password123") + ); + + given() + .contentType(ContentType.JSON) + .body(request) + .post("/__users"); + + given() + .when() + .get("/__users/tester2") + .then() + .statusCode(200) + .body("username", is("tester2")) + .body("role", is("OWNER")); + } + + @Test + @DisplayName("Should return 403 when accessing protected endpoint without proper role") + @TestSecurity(user = "common", roles = "USER") + void shouldReturnForbidden() { + given() + .when() + .get("/__users/tester") + .then() + .statusCode(403); + } +} diff --git a/backend/src/test/java/dev/flima/presentation/rest/users/UserValidationTest.java b/backend/src/test/java/dev/flima/presentation/rest/users/UserValidationTest.java new file mode 100644 index 0000000..2597240 --- /dev/null +++ b/backend/src/test/java/dev/flima/presentation/rest/users/UserValidationTest.java @@ -0,0 +1,85 @@ +package dev.flima.presentation.rest.users; + +import dev.flima.application.users.dtos.request.UserDTORequest; +import dev.flima.domain.users.Password; +import dev.flima.domain.users.Role; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasItem; + +@QuarkusTest +class UserValidationTest { + + @Test + @DisplayName("Should return 400 when creating user with invalid email") + void shouldReturnErrorForInvalidEmail() { + UserDTORequest request = new UserDTORequest( + "invalid_email_user", + "Invalid", + "Email", + "not-an-email", + Role.OWNER, + new Password("password123") + ); + + given() + .contentType(ContentType.JSON) + .body(request) + .when() + .post("/__users") + .then() + .statusCode(400) + .body("message", is("Validation failed")) + .body("details", hasItem(is("create.userDTO.email: Please provide a valid email address."))); + } + + @Test + @DisplayName("Should return 400 when creating user with short password") + void shouldReturnErrorForShortPassword() { + UserDTORequest request = new UserDTORequest( + "short_pass_user", + "Short", + "Pass", + "short@example.com", + Role.OWNER, + new Password("123") + ); + + given() + .contentType(ContentType.JSON) + .body(request) + .when() + .post("/__users") + .then() + .statusCode(400) + .body("message", is("Validation failed")) + .body("details", hasItem(is("create.userDTO.password.password: Password does not meet the minimum security requirements."))); + } + + @Test + @DisplayName("Should return 400 when creating user with null fields") + void shouldReturnErrorForNullFields() { + UserDTORequest request = new UserDTORequest( + null, + "", + "Last", + "null@example.com", + Role.OWNER, + new Password("password123") + ); + + given() + .contentType(ContentType.JSON) + .body(request) + .when() + .post("/__users") + .then() + .statusCode(400) + .body("message", is("Validation failed")); + } +} diff --git a/backend/src/test/resources/application-test.properties b/backend/src/test/resources/application-test.properties new file mode 100644 index 0000000..6da51fd --- /dev/null +++ b/backend/src/test/resources/application-test.properties @@ -0,0 +1,12 @@ +# Test profile configurations +quarkus.mailer.mock=true + +# Database generation for tests +quarkus.hibernate-orm.database-generation=drop-and-create + +# Security Test Config +mp.jwt.verify.issuer=https://flima.dev + +# Kafka Resilience Config +mp.messaging.incoming.contact-in.failure-strategy=dead-letter-queue +mp.messaging.incoming.contact-in.dead-letter-queue.topic=contact-messages-dlq diff --git a/backend/src/test/resources/db/test-migration/V999__Test_Casts.sql b/backend/src/test/resources/db/test-migration/V999__Test_Casts.sql new file mode 100644 index 0000000..80d8367 --- /dev/null +++ b/backend/src/test/resources/db/test-migration/V999__Test_Casts.sql @@ -0,0 +1,8 @@ +-- Adiciona casts implícitos para permitir que o Hibernate envie Strings para colunas de ENUM no PostgreSQL +-- Isso resolve o erro "column is of type role_enum but expression is of type character varying" sem alterar o código Java. + +CREATE CAST (varchar AS role_enum) WITH INOUT AS IMPLICIT; +CREATE CAST (varchar AS education_type_enum) WITH INOUT AS IMPLICIT; +CREATE CAST (varchar AS status_message_enum) WITH INOUT AS IMPLICIT; +CREATE CAST (varchar AS stack_type_enum) WITH INOUT AS IMPLICIT; +CREATE CAST (varchar AS section_type_enum) WITH INOUT AS IMPLICIT; diff --git a/frontend/src/pages/__snapshots__/Experience.test.jsx.snap b/frontend/src/pages/__snapshots__/Experience.test.jsx.snap new file mode 100644 index 0000000..e122a9a --- /dev/null +++ b/frontend/src/pages/__snapshots__/Experience.test.jsx.snap @@ -0,0 +1,108 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Experience Page Regression > matches DOM snapshot when loaded 1`] = ` +
+
+

+ RUNTIME_HISTORY +

+

+ A chronological log of my professional execution states and deployed responsibilities. +

+
+
+
+
+
+
+
+
+

+ + dns + + Mock Architect +

+
+ @ + Mock Corp +
+
+
+ 2020 - 2024 +
+
+
    +
  • + + > + + Mock bullet 1 +
  • +
  • + + > + + Mock bullet 2 +
  • +
+
+
+ + React + + + Redux + +
+
+
+
+
+
+
+`; diff --git a/frontend/src/test/components/Footer.test.jsx b/frontend/src/test/components/Footer.test.jsx new file mode 100644 index 0000000..9c63885 --- /dev/null +++ b/frontend/src/test/components/Footer.test.jsx @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest'; +import { screen } from '@testing-library/react'; +import Footer from '../../components/Footer'; +import { renderWithProviders } from '../utils'; + +describe('Footer Component', () => { + it('renders the dynamic copyright text', () => { + renderWithProviders(